今天听人分享了关于threadLocal的内存泄露知识
ref:http://blog.csdn.net/wudiyong22/article/details/52141608 深入分析 ThreadLocal 内存泄漏问题
ref:话说ReferenceQueue 前半部分不错,后半部分也是模模糊糊,不看也罢
ref:Java引用类型原理剖析 这个是我目前查到唯一讲到referent到底怎么被置为null的文章
ref:gc过程中reference对象的处理
一、前置知识
根源在于内存是宝贵的,因此我们需要合理使用弱引用。由于java的GC策略帮我们已经完成了很多优化,大部分情况下,我们自己无需使用弱引用,但还是要理解这个概念
1.1 弱引用类 WeakReference<T>
想看最基本的使用方法
productA = new Product(...);
WeakReference<Product> weakProductA = new WeakReference<>(productA);
但基本没见过直接使用的,Java类库为我们提供了WeakHashMap类。
weakhashmap首先是一个hashmap,外层是一个Entry[k,v] table,table[i]是一个链表,每一个entry指向next entry
那么它相比于普通的HashMap区别在哪?
最特殊的就是Map的这个 Entry是继承自WeakRenferenced的,super(key,queue)
此处最难以理解,容我自己理理,我也被绕了很久
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;
}
- WeakReference这个基类到底有什么特性呢?
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
其实就简单的有一个引用队列ReferenceQueue<? super T>,有一个引用referent。
但我一开始困惑的原因就是,我想当然认为entry是WeakReference的子类,所以这个引用是Entry
!错了
Entry显式调用了super(key, queue);
Object Key才是整整的referent
1.2 引用类 Reference<T>
继续看WeakReference
先看看ReferenceQueue在Java中的描述:
Reference queues, to which registered reference objects are
appended by the garbage collector after the appropriate reachability changes are detected.
中文JavaDoc的描述:引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中
查看源代码会发现它很简单,实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的。
敲黑板,前文提到的注册queue的意义就在这里
这个queue是通过构造函数传入的,表示创建一个Reference时,要将其注册到那个queue上。
Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:
Active:
queue = ReferenceQueue with which instance is registered,
or ReferenceQueue.NULL if it was not registered with a queue; next = null.
Pending:
queue = ReferenceQueue with which instance is registered;
next = Following instance in queue, or this if at end of list.
Enqueued:
queue = ReferenceQueue.ENQUEUED; next = Following instance
in queue, or this if at end of list.
Inactive:
queue = ReferenceQueue.NULL; next = this.
1)pending和 discovered成员:都和GC有关
先看pending对象
/* 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.
*/
private static Reference pending = null;
//这个对象,定义为private,并且全局没有任何给它赋值的地方,
//根据它上面的注释,我们了解到这个变量是和垃圾回收期打交道的。
再看discovered,同样为private,上下文也没有任何地方使用它
transient private Reference<T> discovered; /* used by VM */
//看到了它的注释也明确写着是给VM用的。
上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscovery和RefeferentBasedDiscovery两种策略)
2)ReferenceHandler线程
这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。
通过这2点,我们来看整个过程:
! pending内对象的放入是由jvm 的 GC 来完成
1.垃圾收集会对各类引用进行清理,具体可以参考Java引用类型原理剖析
这里就讲WeakHashMap关联的wearReference发生了什么
2.ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULL,Handler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。
ReferenceQueue.NULL相当于我们提供了一个空的Queue去监听垃圾回收器给我们的反馈,并且对这种反馈不做任何处理。要处理反馈,则必须要提供一个非ReferenceQueue.NULL的queue。
1.3 到此我们来看Young GC的时候,到底发生了什么?
主要分为Native层和Java层两个部分。
- Native层在GC时将需要被回收的Reference对象加入到DiscoveredList中(代码在
referenceProcessor.cpp
中process_discovered_references
方法),然后将DiscoveredList的元素移动到PendingList中(代码在referenceProcessor.cpp
中enqueue_discovered_ref_helper
方法),PendingList的队首就是Reference类中的pending对象 -
敲黑板,重点java中ReferenceQueue的poll方法
Java层流程比较简单:就是源源不断的从PendingList中提取出元素,然后将其加入到ReferenceQueue中去,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。
/**
* Polls this queue to see if a reference object is available. If one is
* available without further delay then it is removed from the queue and
* returned. Otherwise this method immediately returns <tt>null</tt>.
*
* @return A reference object, if one was immediately available,
* otherwise <code>null</code>
*/
public Reference<? extends T> poll() {
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
可以看到,当你可达时,会从queue中remove并返回,但如果不可达,会返回null,后面会用到
总流程
reference-JVM->pending-JAVA->referenceQueue
以WeakReference进行分析
1.GC refs_lists
中存放了本次GC发现的某种引用类型(虚引用、软引用、弱引用等)
2.process_discovered_reflist
方法的作用就是将不需要被回收的对象从refs_lists
移除掉,refs_lists
最后剩下的元素全是需要被回收的元素,最后会将其第一个元素赋值给上文提到过的Reference.java#pending
字段。
3.WeakReference在Java层只是继承了Reference,没有做任何的改动。那referent字段是什么时候被置为null的呢?要搞清楚这个问题我们再看下上文提到过的process_discovered_reflist
方法:
// Phase 3:
// 根据clear_referent的值决定是否将不存活对象回收
if (mt_processing) {
RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
task_executor->execute(phase3);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase3(refs_lists[i], clear_referent,
is_alive, keep_alive, complete_gc);
}
}
return total_list_count;
}
void
ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
ResourceMark rm;
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
iter.update_discovered();
iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
接下去是重点,看到没有,是把referent变为了null!!!总算可以确定了
clear_referent这个值,各种不同引用是不同的
if (clear_referent) {
// NULL out referent pointer
//将Reference的referent字段置为null,之后会被GC回收
iter.clear_referent();
} else {
// keep the referent around
//标记引用的对象为存活,该对象在这次GC将不会被回收
iter.make_referent_alive();
}
...
}
...
}
二、WeakHashMap是如何实现的?
有了以上的知识,让我们从头到晚重新理一下WeakHashMap到底如何实现的
2.1 WeakHashMap.put 一个对象
1.首先 weakHashMap有一个属性 ReferenceQueue<Object>,用处上文已经讲过,它的目的是最终用于new Entry时作为queue
tab[i] = new Entry<>(k, value, queue, h, e);
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;
}
这里的k就是Object key,会作为referent注册到queue中
上文已经分析过,wakeReference会清除其中的referent,那么entry本质上是weakreference,它也有ReferenceQueue<Object>,它也会清除其中的referent,这里的referent是什么,就是key,所以weakhashmap的entry的key会被清除
2.2 JVM发生GC
JVM发生GC,对所有的WeakReference进行分析,那么Entry会被回收,按基础知识中的逻辑,我们知道GC会把WeakReference的referent置为null,由于Entry的构造函数调用了super<k,queue>,所以GC把Entry的referent也就是key置为null
注意!然后把Entry放到了reference 的pending中,因为Entry才是真正的WeakReference啊
到了这步位置,其实说穿了,只是把key清除了
2.3 Java的ReferenceHandler
Java的ReferenceHandler将pending enqueue到了referenceQueue中
2.4 OK,接下去就是把Entry当中的Value回收部分了,这部分比较简单好理解了
- WeakHashMap的expungeStaleEntries方法
此方法会在3个地方进行调用
getTable(),这个getTable基本上会在所有的get,put等方法中进行调用
resize()
size()
顾名思义,这个方法就是删除陈旧的数据。前面ReferenceQueue中提到了,对象不可达时,poll方法会返回null,但这返回null只是返回,真正目前虚拟机中,只有key为显式置为null了,后面还要把value显示置为null
NICE,总算全部通了!
/**
* 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;
}
}
}
}