及时清理过期的对象引用

时间:2021-11-29 23:27:43

虽然Java有JVM层面的GC帮助内存回收,但是当对象被引用但是实际上已经无效时GC没法清理。如下例所示

 1 public class Stack {
 2 
 3     private Object[] elements;
 4     private int size = 0;
 5     private static final int DEFAULT_INITIAL_CAPACITY = 16;
 6 
 7     public Stack() {
 8         elements = new Object[DEFAULT_INITIAL_CAPACITY];
 9     }
10 
11     public void push(Object e) {
12         ensureCapacity();
13         elements[size++] = e;
14     }
15 
16     public Object pop() {
17         if (size == 0)
18             throw new EmptyStackException();
19         return elements[--size];
20     }
21 
22     private void ensureCapacity() {
23         if (elements.length == size)
24             elements = Arrays.copyOf(elements, 2 * size + 1);
25     }
26 
27 }

上面代码看起来没什么问题,但是缺存在内存泄漏问题,因为存在过期的对象引用(无用却又没有释放),本例中elements中下标大于size的元素都是不会被用到但又被elements引用而不会被GC清理。当elements的容量很大的情况下,势必影响内存的利用。所以应该及时清理这些过期的引用:

public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object e = elements[--size];
        elements[size] = null; // 清理无效的引用,方便GC进行垃圾清理
        return e;
}

在修改后的pop方法中,每当元素被取出后及时清理elements对元素的引用。

内存泄露的另一个常见来源是缓存。当放入缓存中的对象长时间没有被引用实际上是可以被清理掉的,所以针对这种需求,可以使用WeakHashMap,WeakHashMap的特点是当除了WeakHashMap自身引用key外没有任何地方引用key,那么这个key对应的Entry将会从WeakHashMap中移除从而方便GC回收。

 1 public class WeakHashMapTest{
 2 
 3     public static void main(String[] args) {
 4 
 5         Map<Object, String> wm = new WeakHashMap<>();
 6         Object k1 = new Object();
 7         Object k2 = new Object();
 8         wm.put(k1, "k1_value");
 9         wm.put(k2, "k2_value");
10 
11         k1 = null;
12 
13         System.gc();
14 
15         wm.forEach((k, v) -> System.out.println(v));
16 
17     }
18 
19 }
程序运行结果:
k2_value

通过上例可以看到,当k1,k2做为key存入WeakHashMap后,让k1为null即手动使k1对应的对象的引用持有者只有WeakHashMap,此时执行GC后输出WeakHashMap中元素发现只剩下k2了。

在这里简单说说WeakHashMap原理。

WeakHashMap底层也是使用Entry存储元素,但是这个Entry继承了WeakReference,如下所示

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue); // 使用key作为被WeakReference引用的对象
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
}

在看看WeakReference单独使用时的简单案例

public class WeakReferenceTest{

    public static void main(String[] args) {

        Object o = new Object(); // 被弱引用关联的对象
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        WeakReference<Object> w = new WeakReference(o, queue);

        o = null;
        System.gc();

        System.out.println(w);
        System.out.println(queue.poll());
        System.out.println(w.get());

    }

}
程序运行结果:
java.lang.ref.WeakReference@1b6d3586
java.lang.ref.WeakReference@1b6d3586
null
public class WeakReferenceTest{

    public static void main(String[] args) {

        Object o = new Object(); // 被弱引用关联的对象
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        WeakReference<Object> w = new WeakReference(o, queue);

//        o = null;
        System.gc();

        System.out.println(w);
        System.out.println(queue.poll());
        System.out.println(w.get());

    }

}
程序运行结果:
java.lang.ref.WeakReference@1b6d3586
null
java.lang.Object@4554617c

通过对比上面两次运行结果可以得知:当被WeakReference保存的o不存在除了WeakReference外的其他引用时执行GC后,WeakReference将会被回调存储到ReferenceQueue中。所以通过ReferenceQueue.pop得到的WeakReference中保存的对象就都可以看作是被GC清理掉的(虽然此时调用WeakReference.get得到的都是null,但是可以知道通过ReferenceQueue.pop得到的WeakReference中保存的对象已经被GC清理掉了)。

结合上面简单的WeakReference用法实例再回头看WeakHashMap中Entry的源码中的如下片段

Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue); // 将Map中的key作为WeakReference保存的对象
            this.value = value;
            this.hash  = hash;
            this.next  = next;
}

上面是Entry的构造器代码,里面手动调用了WeakReference的构造器(super(key, queue)),将key作为WeakReference保存的对象。那么当key除了被WeakHashMap引用外没有被其他的对象引用时执行GC后key会被回收然后Entry被回调存到ReferenceQueue中。

综上所述也只是清理了key,但是作为Map而言仅仅清理key并不能防止内存泄露,因为Entry和value都还在。所以必须Entry和value都被清理才算是完美。在WeakHashMap中清理Entry和value的代码如下:

 1 private void expungeStaleEntries() {
 2         for (Object x; (x = queue.poll()) != null; ) {
 3             synchronized (queue) {
 4                 @SuppressWarnings("unchecked")
 5                     Entry<K,V> e = (Entry<K,V>) x;
 6                 int i = indexFor(e.hash, table.length);
 7 
 8                 Entry<K,V> prev = table[i];
 9                 Entry<K,V> p = prev;
10                 while (p != null) {
11                     Entry<K,V> next = p.next;
12                     if (p == e) {
13                         if (prev == e)
14                             table[i] = next;
15                         else
16                             prev.next = next;
17                         // Must not null out e.next;
18                         // stale entries may be in use by a HashIterator
19                         e.value = null; // Help GC
20                         size--;
21                         break;
22                     }
23                     prev = p;
24                     p = next;
25                 }
26             }
27         }
28     }

简单来说就是通过ReferenceQueue.pop取出被GC清理过WeakReference所保存key的WeakReference(在WeakHashMap中WeakReference即Entry,因为Entry继承了WeakReference,所以ReferenceQueue.pop取出的就已经是Entry)。得到Entry后在table中找到Entry然后移动链表使Entry不被引用同时清理Entry中保存的value(e.value = null; // Help GC)。经过这些就清理了Entry和value以及key,从而实现了WeakHashMap的特色功能(当key不被外部引用时执行GC会从WeakHashMap中清理key对应的Entry)。

不过需要注意的是expungeStaleEntries()方法执行后才会清理Entry以及value,expungeStaleEntries()是在getTable(),size(),resize()中调用,所以WeakHashMap的清理操作是伴随着使用者的使用操作过程中无意识被清理的(在正常使用中可能底层调用了getTable(),size(),resize())。比如下面实例:

// 使用Map的forEach
wm.forEach((k,v)->System.out.println(v));

// WeakHashMap中的forEach实现
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    int expectedModCount = modCount;

    Entry<K, V>[] tab = getTable(); // 底层调用了expungeStaleEntries()从而清理无效的Entry
    for (Entry<K, V> entry : tab) {
        while (entry != null) {
            Object key = entry.get();
            if (key != null) {
                action.accept((K)WeakHashMap.unmaskNull(key), entry.value);
            }
            entry = entry.next;

            if (expectedModCount != modCount) {
                throw new ConcurrentModificationException();
            }
        }
    }
}