使用WeakHashMap避免内存泄露

    Java内存管理是有JVM负责的,一般情况下,开发者不直接进行内存处理,在Java底层,会有内存的自动处理和垃圾回机制。然而,Java也会有内存泄露的问题,一般是在设计层面发生的。

    Java内存泄露?怎么可能?很多Java开发者会晕倒的内存泄露的陷阱,就是Java容器的使用(Collection和Map):

public class UserDataLoader {
private static Map cache = new HashMap();
//...载入对象到cache中,依赖缓存数据。
}

主要有两种情况导致内存泄露:

    1、如果Map本省有强引用存在的话,Map中对象即时不被JVM中的任何地方应用到,也不会被回收。

    2、如果没有适当的管理,这个Map容器很容易失控,不断的增长。

    要避免上面的两种情况,可以在对象不在使用的时候,将它从Map中移除。这就需要容易管理者构造一个清理的函数给对象调用者使用,或者使用一个监听器模式。

    然而,这可能不适合某些特定的场景,比如交互式的扩展API。添加这样的函数会使得使用者的API变得更加复杂,增加类之间的耦合度,不好。

    一个更简洁当实用的方法是使用一个Map的Key作为ID来标记缓存对象,这时Map不再需要GC来回收对象,而是使用编码逻辑,定期对容器进行检查,对Map进行清理,清楚不需要的对象,保持Map容器的容量。

java.util.WeakHashMap出马

    WeakashMap提供一个更优雅的方法解决这个问题。使用WaekHashMap实例化Map。当使用者不再有对象引用的时候,WeakHashMap将自动被移除对应Key值的对象。

    正如Java API中对WeakHashMap的描述--这个容器将不再保护key,不防止对应的key被GC回收。这是什么意思呢?让我们看看项目的代码:

Map<Object, Object> objectMap = new WeakHashMap<Object, Object>();
for (int i = 0; i < 1000; i++) {
    objectMap.put(String.valueOf(i), new Object()); System.gc();
    System.out.println("Map size :" + objectMap.size());
}

    输出的结果是一堆1和0。因为String.valueOf(i)构建了很多对象实例,但是这些对象实例不断的被GC回收掉。注意,这些输入结果是不规则的,有可能是0,1或者2(如果你运气好),因为现实的调用了GC,这会导致WeakHashMap很快会进行并行的回收。

memoryUsage

上图是使用WeakHashMap和HashMap的内存情况的对比。

上面一栏是WeakHashMap的内存使用情况,GC周期性的进行垃圾回收,内存最终停留在100MB左右。

下面一栏是HashMap的内存使用情况,没有显示的清理Map,容量不断地增长,增长到600MB以上,最终导致“OutOfMemoryException”。

WeakHashMap很好!但不能滥用!

1、Integer Key。

Map<Object, Object> objectMap = new WeakHashMap<Object, Object>();
for (int i = 0; i < 1000; i++) {
    objectMap.put(i, new Object());
    System.gc();
    System.out.println("Map size :" + objectMap.size());
}

我们期待得到1和0的输出,但是实际上看到的是许多128!有趣的是,并非所有的东西都是GC感兴趣的。在上面的情景中,int i会被转化为封装类型Integer,但是Integer保留了-128到127的缓存,因此哪些Key <= 127的Entry将不会进行自动回收。所以不要使用原始类型(及其对应的包装类型)。

IntegerAsKey

IntegerAsKey


2、克隆的Key

在Java中克隆后的对象和原来的对象是不同的对象,也是有不同的引用分别指向自身引用和克隆引用。如果,GC回收的判断,只判断Key原始的引用,对Key进行克隆,那就是一个新的对象和引用,GC不会去判断克隆后的对象。

3、容器循环依赖。

如下面的代码:

Object tempObj = new Object();
objectMap.put(tempObj, new MyContainer(tempObj)); 
//MyContainer stores a reference of tempObj in its field
System.gc();
System.out.println("Map size :" + objectMap.size());

输出将显示Map容量不断增长,你可能会说,这样的情况“永远不会发生”!请记住,这样的情况,是有可能会出现的。