从WeakHashMap开始
原创文章 转载需注明出处
2019/4/14
序言
HashMap对象只有在执行remove操作之后才会删除相应Entry. 参考下列代码, 即使k1=null并调用系统GC,hashmap的size仍然为2。HashMap的key是堆中对象的另一个强引用,与例中的k1独立。即使将k1设置为null,hashMap仍然持有一个强引用,从而并不会被GC算法标记清除。
code1
// hash map test
public static void test3(){
HashMap<String, Object> map = new HashMap<>();
String k1 = new String("key1");
String k2 = new String("key2");
map.put(k1, new Object());
map.put(k2, new Object());
k1 = null;
System.gc();
printMap(map); // map的size仍然是2
}
通常在缓存场景下,hashMap会足够大。我们希望不常使用的key-value对能够被自动及时清除,WeakHashMap可以胜任此需求。不出意外的,下列代码map里key为k1的key-value对会被清除。
code2
public static void test4(){
WeakHashMap<String, Object> map = new WeakHashMap<>();
String k1 = new String("key1");
String k2 = new String("key2");
map.put(k1, new Object());
map.put(k2, new Object());
k1 = null;
System.gc();
printMap(map); // k1已经被删除了
}
本文以WeakHashMap为起点,涉及关键点有 Java引用,GC标记,弱引用,JVM
简便起见,后文将WeakHashMap简称为WHM.
WHM原理分析
从实验结果开始分析,代码段2中,为什么将k1赋null以后并调用系统GC就可以清除WHM的key-value对?
主体原因为两点:
其一,如果堆区的对象在栈区中没有强引用,而仅仅只有弱引用,则该堆区对象会在GC算法运行时被清除。
WHM实现上和HashMap的原理大部相同,但是WeakHashMap.Entry的继承了WeakReference类,当Entry的强引用不存在时,Entry的key被GC回收。
其二,JVM的GC算法只清除Entry的KEY,value需要WHM自行处理,这种设计的原因后续分析。WHM在其关键方法expungeStaleEntries
方法中清除value。
上述表述中,第一条为JVM垃圾回收的职责,第二条为WHM职责;则WHM原理可归结为下述三点:
- JVM做了什么;如何做
- WHM做了什么
- JVM和WHM如何协作
WeakHashMap.Entry的继承链为 Entry --> WeakReference --> Reference. Reference类实现了与GC合作的逻辑。
code Entry类构造方法
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
// ReferenceQuene
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
Entry构造方法中调用WeakReference的父构造方法并传入一个ReferenceQueue实例queue,queue实例会传入Reference类。
Reference中有字段pending:
code3
/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to link its elements.
*/
private static Reference<Object> pending = null;
pending存储用于清除对象的引用链表,其在Reference实现中没有赋值逻辑,由JVM完成赋值。 Reference中开启一个后台线程用于监听pending,一旦不空将其添加到ReferenceQueue中。
code4 在Reference的tryHandlePending
方法中,被标记清除的pending入队。
/**
* Try handle pending {@link Reference} if there is one.<p>
* Return {@code true} as a hint that there might be another
* {@link Reference} pending or {@code false} when there are no more pending
* {@link Reference}s at the moment and the program can do some other
* useful work instead of looping.
*
* @param waitForNotify if {@code true} and there was no pending
* {@link Reference}, wait until notified from VM
* or interrupted; if {@code false}, return immediately
* when there is no pending {@link Reference}.
* @return {@code true} if there was a {@link Reference} pending and it
* was processed, or we waited for notification and either got it
* or thread was interrupted before being notified;
* {@code false} otherwise.
*/
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
这里由于ReferenceQueue在WHM中实例化并被传入,则WHM中可以获取ReferenceQueue中被GC添加的引用。WHM的expungeStaleEntries
方法消费ReferenceQueue
中的元素,在table中找到元素对应的Entry并将其删除(基于拉链式实现的Map,删除逻辑类比链表删除)。
需要注意的是,Entry的key对应的对象已经被删除,而e.value维持堆中对象的强引用,置为null以后就会在下次GC中被清除。
code5
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
问题分析
- WHM清除key引用而不清除value引用的实验验证
code6
public static void test5(){
List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
for(int i=0; i<10000;i++){
WeakHashMap<byte[][], byte[][]> weakHashMap = new WeakHashMap<>();
weakHashMap.put(new byte[1000][1000], new byte[1][1]);
maps.add(weakHashMap);
}
}
code7
public static void test5(){
List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
for(int i=0; i<10000;i++){
WeakHashMap<byte[][], byte[][]> weakHashMap = new WeakHashMap<>();
weakHashMap.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(weakHashMap);
}
}
code6并不会造成OOM,而 code7会。 说明WHM在没有触发expungeStaleEntries
时,会把key引用对应的堆区垃圾回收而不会对value引用对应的堆区垃圾回收。原因可有Entry构造函数看出,真正当做WeakReference的引用是是Enty的key而不是Entry全部。
code WeakHashMap类
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
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);
this.value = value;
this.hash = hash;
this.next = next;
}
- WHM为什么只清除Entry.key引用的对象而不直接清除value引用对象?
考虑一种假设场景:如果Entry的构造方法在调用WeakReference构造方法的时候reference参数传入的是Entry本身,而不是key,则在GC发生时Entry被直接删除。而由于Entry的链表式结构,一个Entry被删除之后其之后的Entry都将不可访问,这自然不是理想状态。WeakReference设计允许业务代码使用依赖注入的方法传入一个ReferenceQueue,ReferenceQueue中存放被GC清除对象的引用,业务代码可以用ReferenceQueue中被清除的对象引用个性化处理。当Java代码需要感知GC清除了哪些对象的时候,ReferenceQueue是Java代码和GC沟通的媒介。
总结与联想
- 软引用(soft reference)是当堆空间不足时才会被GC回收;弱引用(weak Reference)则不管空间够不够都会被回收;但对象只要存在一个强引用就都不会被GC回收。 弱引用的这种特性在缓存场景中引用较多,因为开发者也很难确定指定缓存在何时被清除。
- WeakReference应用的另一个著名例子是ThreadLocal,Java中的每个Thread都持有一个ThreadLocalMap,ThreadLocal.set(T)事实上是将ThreadLocal本身作为key,T为value存储在当前Thread的ThreadLocalMap中。这种设计方法导致了ThreadLocalMap的生命周期和Thread一样,且引用被Thread持有,有潜在内存泄漏风险。因此ThreadLocalMap中的ThreadLocal key也设计为弱引用,当threadLocal不存在强引用时,Thread.ThreadLocalMap持有弱引用的ThreadLocal可以被清除(ThreadLocal仍然存在内存泄漏风险,使用时应小心)