WeakHashMap
最近工作中碰到了java.util.WeakHashMap<K, V>,不解其中奥妙,遂查个究竟,顺带记录下来
Java引用类型
首先需要了解Java四种引用类型:
强引用(StrongReference)
强引用是使用最普遍的引用,平时我们常写的A a = new A();就是强引用
GC不会回收强引用,即使内存不足的情况下也不会,宁可OOM
软引用(SoftReference)
SoftReference 的主要特点是具有较强的引用功能。
只有当内存不够的时候才进行回收,而在内存足够的时候,通常不被回收。
另外,引用对象还能保证在 Java 抛出 OutOfMemoryError 之前,被设置为null。
软引用的使用可以参考guava-cache
弱引用(WeakReference)
WeakReference 在垃圾回收器运行时,一定会被回收,而不像 SoftReference 需要条件。但是,若对象的引用关系复杂,则可能需要多次回收才能达到目的。
虚引用(PhantomReference)
PhantomReference 主 要 用 于 辅 助 finalize 方法。
PhantomReference 对象执行完了 finalize 方法后,成为 Unreachable Objects。
但还未被回收,在此时,可以辅助 finalize 进行一些后期的回收工作。
WeakHashMap中怎么实现Weak?
大家都知道Map用Entry存储数据,看看WeakHashMap实现细节
Put部分的代码如下:
Entry<K,V> e = tab[i];
tab[i] = new Entry<K,V>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
下面是WeakHashMap的Entry实现,继承了WeakReference
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
private V value;
private final int hash;
private Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(K key, V value,
ReferenceQueue<K> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
将Key处理成Reference:
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
与HashMap比较一下,Entry不直接引用Key这个对象,而是将引用关系放到了父类WeakReference中,可以看出WeakHashMap将传入的key包装成了WeakReference,并传入了一个ReferenceQueue;但是弱引用的实现细节还是不清楚,接着扒。。。
Key如何清理
Reference类中有一段static代码
static private class Lock { };
private static Lock lock = new Lock();
private static Reference pending = null;
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
线程的优先级设成MAX,是一个什么样的线程需要如此高的权限?pending 、lock 都被static声明,lock.wait之后谁来唤醒,互联网上一顿搜罗,才明白JVM参与了这些事。
用通俗的话把JVM干的事串一下: 假设,WeakHashMap对象里面已经保存了很多对象的引用。JVM使用进行CMS GC的时候,会创建一个ConcurrentMarkSweepThread(简称CMST)线程去进行GC,ConcurrentMarkSweepThread线程被创建的同时会创建一个SurrogateLockerThread(简称SLT)线程并且启动它,SLT启动之后,处于等待阶段。CMST开始GC时,会发一个消息给SLT让它去获取Java层Reference对象的全局锁:lock。直到CMS GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference的pending 属性当中(每次GC完毕之后,pending属性基本上都不会为null了),然后通知SLT释放并且notify全局锁: lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。
想要了解具体细节,再深扒一下openjdk的源码instanceRefKlass.cpp获得lock部分
void instanceRefKlass::acquire_pending_list_lock(BasicLock *pending_list_basic_lock) {
// we may enter this with pending exception set
PRESERVE_EXCEPTION_MARK; // exceptions are never thrown, needed for TRAPS argument
Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock());
ObjectSynchronizer::fast_enter(h_lock, pending_list_basic_lock, false, THREAD);
assert(ObjectSynchronizer::current_thread_holds_lock(
JavaThread::current(), h_lock),
"Locking should have succeeded");
if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION;
}
Gc完成后, pending赋值,lock释放
void instanceRefKlass::release_and_notify_pending_list_lock(
BasicLock *pending_list_basic_lock) {
// we may enter this with pending exception set
PRESERVE_EXCEPTION_MARK; // exceptions are never thrown, needed for TRAPS argument
//
Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock());
assert(ObjectSynchronizer::current_thread_holds_lock(
JavaThread::current(), h_lock),
"Lock should be held");
// Notify waiters on pending lists lock if there is any reference.
if (java_lang_ref_Reference::pending_list() != NULL) {
ObjectSynchronizer::notifyall(h_lock, THREAD);
}
ObjectSynchronizer::fast_exit(h_lock(), pending_list_basic_lock, THREAD);
if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION;
}
lock释放后, ReferenceHandler线程进入正常运转,将 pending 中的Reference对象压入了各自的 ReferenceQueue中
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
上面部分讲了JVM在GC的时候帮我们把WeakHashMap中的key的内存释放掉了,那么 WeakHashMap中Entry数据怎么释放,看看 WeakHashMap的 ReferenceQueue怎么起的作用?
Entry如何清理
当GC之后,WeakHashMap对象里面get、put数据或者调用size方法的时候,WeakHashMap比HashMap多了一个 expungeStaleEntries()方法
private void expungeStaleEntries() {
Entry<K,V> e;
while ( (e = (Entry<K,V>) queue.poll()) != null) {
int h = e.hash;
int i = indexFor(h, 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;
e.next = null; // Help GC
e.value = null; // " "
size--;
break;
}
prev = p;
p = next;
}
}
}
expungeStaleEntries方法 就是将ReferenceQueue列队中的WeakReference依依poll出来去和Entry[]数据做比较,如果发现相同的,则说明这个Entry所保存的对象已经被GC掉了,那么将Entry[]内的Entry对象剔除掉,这样就把被GC掉的 WeakReference对应的Entry从WeakHashMap中移除了。
最后
public static void main(String[] args) {
Map<String, String> data = new WeakHashMap<String, String>();
data.put("123", "123");
data.put("124", "124");
data.put("125", "125");
data.put("126", "126");
System.gc();
data.size();
System.out.println(data);
}
代码中显式的调用System.gc()之后,data中的数据被清空了吗?答案是没有
经过测试一次gc未必能完全回收所有的weakreference对象,weakreference也有可能出现在old区,至于为什么就要看GC的策略