|     java container weak reference phantom reference   |    

问题

实验很简单,创建一个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指向的对象被销毁的过程,一共分三步走:

  1. 执行对象的finalize()方法
  2. 销毁对象
  3. 把PhantomReference插到ReferenceQueue

但实际上这个过程要更加复杂。因为这是一个“并发过程”,其中涉及到好几个线程。看下面这个Finalizable Object life-time的图: finalization

  1. 首先“主线程”(main application)执行System.gc(),建议触发GC。
  2. 接下来Minor GC回收器喊一声stop-the-world,把主线程挂起。开始Mark-Copy算法,标记堆中所有unreachable对象。
  3. 但某个unreachable的对象如果是finalizable的,Minor GC知道不能马上杀掉,需要先执行finalize()方法。但finalize()方法Minor GC自己又不能执行。需要Finalizer的finalizer daemon thread线程负责执行。所以Minor GC没办法,只好先把它插入到finalization queue。等以后什么时候finalizer daemon thread接手了,会一个个执行队列里对象的finalize()方法。
  4. 问题就在这里。插入finalization queue的对象会被finalizer daemon thread后台线程的Finalizer class引用。所以图片里lifetime的第一轮GC,这个对象又被标记回reachable。在这轮回收中幸存下来,从Eden被拷贝到Survivor区。
  5. 这轮GC结束,主线程接管。然后finalizer daemon thread因为优先级比主线程低得多,会在某个不确定的时候执行finalize()方法。然后对象被标记成finalized。这时候对象和Finalizer class之间的强引用才断掉。对象重新变回unreachable。
  6. 这时候还需要第二轮触发GC才能再开启回收过程。因为finalize()方法只能被执行一次,所以第二轮GC会销毁对象。
  7. 对象被销毁了才会被加入到reference queue。

换成WeakReference

如果把实验里的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的速度。

相关文章

《话说ReferenceQueue》 《深入探讨 java.lang.ref 包》