实验很简单,创建一个PhantomReference,马上触发GC。然后,打印查看
[虚引用地址,虚引用指向的对象,被压到引用队列里的引用]。
public class TestPhantom {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> rq = new ReferenceQueue<>();
PhantomReference<Object> pr = new PhantomReference<>(new Object(), rq);
System.out.println(pr+", "+pr.get()+", "+rq.poll());
System.gc();
Thread.sleep(1000);
System.out.println(pr+", "+pr.get()+", "+rq.poll());
}
}
输出也很正常,GC之后,虚引用指向的对象被销毁变成null。引用队列里也找到了虚引用的地址。
java.lang.ref.PhantomReference@15db9742, null, null
java.lang.ref.PhantomReference@15db9742, null, java.lang.ref.PhantomReference@15db9742
问题是,如果重写对象的finalize()方法,再触发GC。结果就很奇怪。
public class TestPhantom {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> rq = new ReferenceQueue<>();
PhantomReference<Object> pr = new PhantomReference<>(new Object() {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize");
}
}, rq);
System.out.println(pr + ", " + pr.get() + ", " + rq.poll());
System.gc();
Thread.sleep(1000);
System.out.println(pr + ", " + pr.get() + ", " + rq.poll());
}
}
打印出了finalize,说明finalize()方法被执行了。但在ReferenceQueue里却找不到被销毁的虚引用地址。
java.lang.ref.PhantomReference@15db9742, null, null
finalize
java.lang.ref.PhantomReference@15db9742, null, null
根本原因,是由于Minor GC对finarable对象的处理是一个复杂的并发过程。其中涉及到多个线程。
先推荐两篇生肉。英文好的同学,答案就在这两篇文章里。其中第一篇是Oracle官网上的,比较权威。 《How to Handle Java Finalization’s Memory-Retention Issues》 – By Tony Printezis 《The Secret Life Of The Finalizer: page 2 of 2》 – By Fasterj
下面我只是简单地描述文章提到的几个关键点。
首先,大家肯定知道当一个类重写了finalize( )方法后(has a non-trival finalize method),这个类的对象会被系统标记成”finalizable”。GC在销毁对象之前,会调用finalize()方法。完了之后再销毁对象。
然后,因为PhantomReference被插入ReferenceQueue队列的时机和WeakReference不同:
所以一般我们会认为,PhantomReference指向的对象被销毁的过程,一共分三步走:
但实际上这个过程要更加复杂。因为这是一个“并发过程”,其中涉及到好几个线程。看下面这个Finalizable Object life-time的图:
如果把实验里的PhantomReference换成WeakReference是不是能找到引用队列里的对象呢?
public class TestPhantom {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> rq = new ReferenceQueue<>();
WeakReference<Object> wr = new WeakReference<>(new Object() {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize");
}
}, rq);
System.out.println(wr + ", " + wr.get() + ", " + rq.poll());
System.gc();
Thread.sleep(1000);
System.out.println(wr + ", " + wr.get() + ", " + rq.poll());
}
}
是的,ReferenceQueue里能够找到WeakReference。
java.lang.ref.WeakReference@15db9742, null, null
finalize
java.lang.ref.WeakReference@15db9742, null, java.lang.ref.WeakReference@15db9742
但引用指向的对象,却已经被销毁了。但按理说不是在执行finalize()的这一轮GC,对象会幸存下来吗?
这又是另外一个坑:
所以这时候,虽然打印弱引用指向的对象是null。但heap里的对象第一次GC过后,并没有被销毁。只不过我们已经无法获得它的引用了。
所以WeakReference被压入引用队列,而且get()显示是null,不保证对象已经被销毁。
只有引用队列里的PhantomReference能保证对象已经被销毁。
所以为什么Joshua Bloch说finalizable对象靠不住,因为回收过程不确定性太大了。本来java触发GC主动权就不在程序员手里,System.gc()只是“建议”触发回收。现在因为finalize的存在,第一次回收还销毁不掉。而且等finalizer daemon thread执行finalize()也是个不确定的事。所以才会有推荐两篇文章中说的Finalization’s Memory-Retention Issues问题。就是finalizer处理的速度跟不上系统产生finalizable object的速度。