Platform
OS: OS X EI Capitan 10.11.6 (15G31)
JAVA version: 1.8.0_45
概述
个人还是比较喜欢图形化的东西,所以这两天在看过Reference之后,就把它的概念图画了出来。
根据Reference这个类里面的注释,整个Reference的继承体系跟GC是紧密联系在一起的。当Reference对象被GC处理过,说明这个Reference所指向的对象已经被回收过了,那么这个Reference对象也应该做相应的处理;如果该Reference对象注册了ReferenceQueue,那么它就会被丢到相应的ReferenceQueue中做回收处理。
Main Idea
这里Reference世界跟GC的接口就是这个Reference.pending,这个每个JVM实例只有一个,用来接受GC的请求。Reference世界中在类加载后就会启动一个无限循环的线程Reference.ReferenceHandler,用来从Reference.pending上取下一个Reference对象,然后把该Reference对象放入它所关联的ReferenceQueue里面。
Reference对象到达自己的队列,谁来处理这些对象呢?那么就要看这些ReferenceQueue是谁来维护的,谁维护,谁处理?在源码中就是跟随着ReferenceQueue的remove方法,看看是谁调用了这个方法,就知道有多少服务方维护了多少ReferenceQueue。
举个例子,我在google的guava-16.0.1中找到了com.google.common.base.internal.Finalizer这个类,它就完成了上面的消费ReferenceQueue的功能。
写到这里,这让我想到了现在在互联网领域里面用的比较多的请求队列。GC相当于客户端,向服务器Reference发送了一个请求,然后服务端并没有同步的处理这些请求,而是放到了请求队列里做异步处理,每个服务端为了处理请求必须创建并维护自己的ReferenceQueue。
代码浅析
当我看源码时,看到了下面的注释。
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
Reference next;
/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
/* 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;
当看到上面这些注释时,真的不知道它在说什么,这些状态的转变真的不是很好理解,但是当我把上面那幅图画出来时,这里说的这些条件就是很好理解的了。
从上图注意到,当Reference对象在Reference.pending队列中时,它们是用discovered
这个成员变量来完成队列的连接的;
这个Reference对象是active的状态,说明这个Reference所指向的对象还没有成为垃圾,还没有被GC过。这个Reference对象还没有进入Reference的世界,它还在GC的监控下呢;当被垃圾回收后,也就是进入Reference的世界后,这个discovered
域被复用作链表的连接,此时并不适用next指针,只是把next指向this。
当Reference对象真正被放入它所注册的ReferenceQueue里时,next指针才被使用,用来连接队列;此时discovered
域被置为null。
更新
今天在查找netty堆外内存的时候,看到了花钱的年华的一篇文章,讲解了netty的堆外内存,其中介绍了堆外内存的回收跟GC相关的地方。这让我想起了当时看代码不是很懂的地方。
private static class ReferenceHandler extends Thread {
public void run() {
for (;;) {
....
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
....
}
}
直接引用他的话吧
当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 “Reference Handler”的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,做爱做的事情——这是一种比finalizer更轻量更好的机制。
请参看我上面给出的那个图,这个Reference Handler就是一个线程,一直监控的这个队列。
DirectByteBuffer的释放
这个就牵扯到堆外内存的释放,这里我们看DirectByteBuffer的构造函数
// Primary constructor
//
DirectByteBuffer(int cap) { // package-private
......................
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
其它的代码略去不表,注意,这里为这个DirectByteBuffer关联了一个Cleaner,查看这个Cleaner你会发现它是一个PhantomReference
package sun.misc;
....
public class Cleaner extends PhantomReference<Object> {
.....
}
那么在这里,DirectByteBuffer的释放就说的通了,在当前系统中发现除了PhantomReference之外没有别的对象对这个DirectByteBuffer对象进行引用时,就会把它放进 Reference类pending list里,之后的事情,见上面我引用“花钱的年华”的部分。
另外需要说明的是,对外内存的回收需要依赖System.gc(),也就是full GC.
更新(2018-2-18)
最近有关注到关于java中,软引用,弱引用和虚引用,有了 新的认识,再次看到自己之前写的这篇,觉得当初还是没有很深刻的理解这些引用的区别,现在终于有所理解,所以做此更新。
其实之前已经对这三个引用有些了解,现在对它们的使用场景做总结如下:
1. 软引用,一般使用于一些内存敏感性应用,做cache使用。被软引用的对象,在JVM抛出OutOfMemoryError之前,将会被回收掉。它的这个特性使得它做为进程内部的cache最为合适。
2. 弱引用,一般用于保存一些跟对象生命周期相同的一些信息,一旦这些对象呗垃圾回收,我们就可以对这些对象进行回收。这个是怎么做到的呢?基本上还是由于Reference所提供的功能有关。去看Reference的构造函数你会发现,它会要求传入一个ReferenceQueue,而这个Queue就是为了提供一种机制,这种机制允许程序员在对象被垃圾回收之前被通知到,而做出一些业务逻辑。而这种机制则弥补了JVM的finalizer机制的不足。因为JVM的finalizer机制并不保证finalizer一定会被调用到,这样当我们希望一个对象生命周期结束后它的相关资源也被释放的逻辑就不能够被保证。而且据说(为证实),finalizer是通过JNI实现,通过JVM调用开销也比较大。而JVM通过提供这种Reference机制,从而为程序员提供了处理资源回收的另外一种方法。
3. 虚引用,也是可以提供这种对象被垃圾回收时的通知机制。只不过跟上面两个的区别是,PhantomReference的get方法被实现了一直返回null。这就意味着,当你通过ReferenceQueue拿到PhantomReference的通知时,你是不能再对它做出一个强引用的,你只能更具需要做出自己的cleanup的逻辑。这也说明了PhantomReference是pre-mortem的,也就是说实在真正finalize之前的。如果通过get方法可以获得对象本身,而后又对其建立一个强引用,则在后面的垃圾回收处理时,就会出错。所以干脆这里不让程序员获得对象的引用。而SoftReference和WeakReference应该就是在JVM的finalize机制之后的。
更新(2018-3-26)
今天吃饭的时候想到一个问题,就是对于PhantomReference
,它的get
方法是返回的null的,如果是这样,我通过ReferenceQueue
的方式即使能拿到这个PhantomReference
,也还是不能对这个对象做实质的资源回收啊。那到底要怎么做才能回收与它的referent
想关联的资源呢?
在网上搜索了一下,当看到例子的时候才恍然大悟
public class LargeObjectFinalizer extends PhantomReference<Object> {
public LargeObjectFinalizer(
Object referent, ReferenceQueue<? super Object> q) {
super(referent, q);
}
public void finalizeResources() {
// free resources
System.out.println("clearing ...");
}
}
然后,在利用这个LargeObjectFinalizer
关联到我们的referent
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
List<LargeObjectFinalizer> references = new ArrayList<>();
List<Object> largeObjects = new ArrayList<>();
for (int i = 0; i < 10; ++i) {
Object largeObject = new Object();
largeObjects.add(largeObject);
references.add(new LargeObjectFinalizer(largeObject, referenceQueue));
}
largeObjects = null;
System.gc();
Reference<?> referenceFromQueue;
for (PhantomReference<Object> reference : references) {
System.out.println(reference.isEnqueued());
}
while ((referenceFromQueue = referenceQueue.poll()) != null) {
((LargeObjectFinalizer)referenceFromQueue).finalizeResources();
referenceFromQueue.clear();
}
上面的这个例子很简单,但是足够可以说明问题,当垃圾回收工作时,会把我们的LargeObjectFinalizer
放到我们提供的referenceQueue
上,然后,我们从它上面取下我们的LargeObjectFinalizer
,调用资源回收方法finalizeResources
。
我觉得这个算是使用PhantomReference
做垃圾回收的一个典型例子了。我们并不是直接使用PhantomReference
,而是通过扩展它,将我们要关注的对象,比如这里的Object
,以及跟该对象相关联的一些资源,关联到我们的LargeObjectFinalizer
中,当然这是需要修改它的构造函数,或者通过其它的一些方式关联到这个LargeObjectFinalizer
里面的。只有这样,我们才能通过这个ReferenceQueue
的机制,达到垃圾回收的目的。
参考
- https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references
- http://javarevisited.blogspot.hk/2014/03/difference-between-weakreference-vs-softreference-phantom-strong-reference-java.html
- http://calvin1978.blogcn.com/articles/directbytebuffer.html
- http://lovestblog.cn/blog/2015/05/12/direct-buffer/
- https://dzone.com/articles/weak-soft-and-phantom-references-in-java-and-why-they-matter
- http://www.baeldung.com/java-phantom-reference