虽然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(); } } } }