深入理解ReferenceQueue GC finalize Reference

        先说一些基本的东西,GC只负责对象内存相关的清理,其他资源如文件句柄,db连接需要手动清理,以防止系统资源不足崩溃。System.gc()只是建议jvm执行GC,但是到底GC执行与否是由jvm决定的。 

        一个正常的对象的生命周期:当新建一个对象时,会置位该对象的一个内部标识finalizable,当某一点GC检查到该对象不可达时,就把该对象放入finalize queue(F queue),GC会在对象销毁前执行finalize方法并且清空该对象的finalizable标识。 

        简而言之,一个简单的对象生命周期为,Unfinalized Finalizable Finalized Reclaimed。 

        Reference中引用的object叫做referent。 

1.先看一个对象finalize的顺序问题

package com.bijian.study;

class A {
	B b;
	public void finalize() {
		System.out.println("method A.finalize at " + System.nanoTime());
	}
}

class B {
	public void finalize() {
		System.out.println("method B.finalize at " + System.nanoTime());
	}
}

public class Test {
	
	public static void main(String[] args) {
		A a = new A();
		a.b = new B();
		a = null;
		System.gc();
	}
}

        原作者的分析如下:


        但我在JDK1.8上Debug控制线程的执行顺序的结果来看,只存在两种结果:

        a.控制台没有任何输出,说明GC线程未来得及执行,主线程就执行结束了,而GC线程由于是守护线程所以也就自动结束了。

        b.控制台输出如下内容:

method B.finalize at 2091777063532
method A.finalize at 2102345356491

        我觉得GC线程回收对象时,会先执行关联对象的finalize()方法,再执行本对象的finalize()方法。[当然,这里也只是我根据运行结果的一种推测,也许正像原作者所说的finalize是乱序执行的]

 

2.对象再生及finalize只能执行一次

package com.bijian.study;

class B {

	static B b;

	public void finalize() {
		System.out.println("method B.finalize");
		b = this;
	}
}

public class Test {
	
	public static void main(String[] args) {
		B b = new B();
		b = null;
		System.gc();
		B.b = null;
		System.gc();
	}
}

        运行结果如果有输出,输出结果如下:

method B.finalize

        对象b本来已经被置null,GC检查到后放入F queue,然后执行了finalize方法,但是执行finalize方法时该对象赋值给一个static变量,该对象又可达了,此之谓对象再生。 

        后来该static对象也被置null,然后GC,可以从结果看到finalize方法只运行了1次。为什么呢,因为第一次finalize运行过后,该对象的finalizable置为false了,所以该对象即使以后被gc运行,也不会执行finalize方法了。 

        很明显,对象再生是一个不好的编程实践,打乱了正常的对象生命周期。但是如果真的需要这么用的话,应该用当前对象为原型重新生成一个对象使用,这样以后这个新的对象还可以被GC运行finalize方法。 

3.SoftReference WeakReference 

        SoftReference会尽量保持对referent的引用,直到JVM内存不够,才会回收SoftReference的referent。所以这个比较适合实现一些cache。 

        WeakReference不能阻止GC对referent的处理。 

4.PhantomReference 

        幻影引用,幽灵引用,呵呵,名字挺好听的。 

        奇特的地方,任何时候调用get()都是返回null。那么它的用处呢,单独好像没有什么大的用处,所以要结合ReferenceQueue。 

5.ReferenceQueue 

        ReferenceQueue WeakReference PhantomReference都有构造函数可以传入ReferenceQueue来监听GC对referent的处理。 

package com.bijian.study;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

class A {
}

public class Test {
	
	public static void main(String[] args) {
		
		ReferenceQueue queue = new ReferenceQueue();
		WeakReference ref = new WeakReference(new A(), queue);
		System.out.println(ref.get());

		Object obj = null;
		obj = queue.poll();
		System.out.println(obj);

		System.gc();

		System.out.println(ref.get());
		obj = queue.poll();
		System.out.println(obj);
	}
}

        运行结果:

com.bijian.study.A@139a55
null
null
java.lang.ref.WeakReference@1db9742

        分析,在GC运行时,检测到new A()生成的对象只有一个WeakReference引用着,所以决定回收它,首先clear WeakReference的referent,然后referent的状态为finalizable,同时或者一段时间后把WeakReference放入监听的ReferenceQueue中。 

        注意有时候最后的System.out.println(obj);有时会失败,因为还没有来的及把WeakReference放入监听的ReferenceQueue中。 

        换成PhantomReference试试:

package com.bijian.study;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

class A {
}

public class Test {
	
	public static void main(String[] args) throws InterruptedException {
		
		ReferenceQueue queue = new ReferenceQueue();
		PhantomReference ref = new PhantomReference(new A(), queue);

		System.out.println(ref.get());

		Object obj = null;
		obj = queue.poll();

		System.out.println(obj);

		System.gc();

		Thread.sleep(10000);

		System.gc();

		System.out.println(ref.get());
		obj = queue.poll();
		System.out.println(obj);
	}
}

        运行结果:

null
null
null
java.lang.ref.PhantomReference@139a55

        貌似和WeakReference没有什么区别呀,别急,还是有个细微的区别的,SoftReference和WeakReference在GC对referent状态改变时,先clear SoftReference/WeakReference对referent的引用,对应的referent状态为Finalizable,只是可以放入F queue,然后把SoftReference/WeakReference放入ReferenceQueue。 

        而PhantomReference当GC对referent的状态改变时,在把PhantomReference放入ReferenceQueue之前referent已经被GC处理到Reclaimed了,即该referent被销毁了。 

        搞了这么多,有什么用?可以使用PhantomReference更好的控制一些关于对象生命周期的事情,当WeakReference放入ReferenceQueue时,并不能保证该referent是被销毁了。别忘了对象可以在finalize方法里再生。而使用PhantomReference,当在ReferenceQueue中发现PhantomReference时,可以保证referent已经被销毁了。 

package com.bijian.study;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

class A {
	static A a;

	public void finalize() {
		a = this;
	}
}

public class Test {

	public static void main(String[] args) throws InterruptedException {

		ReferenceQueue queue = new ReferenceQueue();

		WeakReference ref = new WeakReference(new A(), queue);

		System.out.println(ref.get());

		Object obj = null;

		obj = queue.poll();

		System.out.println(obj);

		System.gc();

		Thread.sleep(10000);

		System.gc();

		System.out.println(ref.get());

		obj = queue.poll();

		System.out.println(obj);
	}
}

        运行结果:

com.bijian.study.A@139a55
null
null
java.lang.ref.WeakReference@1db9742

        即使new A()出来的对象再生了,在queue中还是可以看到WeakReference。 

        将上面实例改成PhantomReference如下:

package com.bijian.study;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

class A {
	static A a;

	public void finalize() {
		a = this;
	}
}

public class Test {

	public static void main(String[] args) throws InterruptedException {

		ReferenceQueue queue = new ReferenceQueue();

		PhantomReference ref = new PhantomReference(new A(), queue);

		System.out.println(ref.get());

		Object obj = null;

		obj = queue.poll();

		System.out.println(obj);

		// 第一次gc

		System.gc();

		Thread.sleep(10000);

		System.gc();

		System.out.println(ref.get());

		obj = queue.poll();

		System.out.println(obj);

		A.a = null;

		// 第二次gc

		System.gc();

		obj = queue.poll();

		System.out.println(obj);
	}
}

        运行结果:

null
null
null
null
java.lang.ref.PhantomReference@6d8acf

        第一次gc后,由于new A()的对象再生了,所以queue是空的,因为对象没有销毁。 

        当第二次gc后,new A()的对象销毁以后,在queue中才可以看到PhantomReference。 

        所以PhantomReference可以更精细的对对象生命周期进行监控。 

Q&A 

Q1:为什么UT会Fail?不是说对象会重生吗,到底哪里有问题? 

public class Test {

	static Test t;

	@Override
	protected void finalize() {
		System.out.println("finalize");
		t = this;
	}
}

	public void testFinalize() {
		Test t = new Test();
		Assert.assertNotNull(t);
		t = null;
		System.gc();
		Assert.assertNull(t);
		Assert.assertNotNull(Test.t);
	}

        A对象是会重生不错。 这里会Fail有两个可能的原因,一个是gc的行为是不确定的,没有什么会保证gc运行。呵呵,我承认,我在console上看到东西了,所以我知道gc运行了一次。 

        另一个问题是gc的线程和我们跑ut的线程是两个独立的线程。即使gc线程里对象重生了,很有可能是我们跑完ut之后的事情了。这里就是时序问题了。 
public void testFinalize() throws Exception {
	Test t = new Test();
	Assert.assertNotNull(t);
	t = null;
	System.gc();
	Assert.assertNull(t);

	// 有可能fail.
	Assert.assertNull(Test.t);
	// 等一下gc,让gc线程的对象重生执行完。
	Thread.sleep(5000);
	// 有可能fail.
	Assert.assertNotNull(Test.t);
}

        这个ut和上面那个大同小异。 

        一般情况下,code执行到这里,gc的对象重生应该还没有发生。所以我们下面的断言有很大的概论是成立的。 

// 有可能fail.  
Assert.assertNull(Test.t);  

        让ut的线程睡眠5秒,嗯,gc的线程有可能已经执行完对象重生了。所以下面这行有可能通过测试。 

Assert.assertNotNull(Test.t);

        嗯,测试通过。但是没有人确保它每次都通过。所以我两处的注释都声明有可能fail。 

        这个例子很好的说明了如何在程序中用gc和重生的基本原则。 依赖gc会引入一些不确定的行为。重生会导致不确定以及有可能的时序问题。 

        所以一般我们不应该使用gc和重生,但是能深入的理解这些概念又对我们编程有好处。 

        这两个测试如果作为一个TestSuite跑的话,情况又会有不同。因为第一个测试失败之后和第二个测试执行之间,gc执行了对象重生。如此,以下断言失败的概率会升高。 

// 有可能fail.  
Assert.assertNull(Test.t);

Q2:关于finalize和ReferenceQueue的关系 

        用小程序验证一下:

public class Tem {

	public static void main(String[] args) throws Exception {

		ReferenceQueue queue = new ReferenceQueue();
		// SoftReference ref = new SoftReference(new B(), queue);
		// WeakReference ref = new WeakReference(new B(), queue);
		PhantomReference ref = new PhantomReference(new B(), queue);
		while (true) {
			Object obj = queue.poll();
			if (obj != null) {
				System.out.println("queue.poll at " + new Date() + " " + obj);
				break;
			}
			System.gc();
			System.out.println("run once.");
		}

		Thread.sleep(100000);
	}

}

class B {

	@Override
	protected void finalize() throws Throwable {
		System.out.println("finalize at " + new Date());
	}
}

        在classB的finalize上打断点,然后让ref分别为SoftReference/WeakReference/PhantomReference,可以看到:

        SoftReference/WeakReference都是不需要finalize执行就可以enqueue的。

        当heap对象的finalize()方法被运行而且该对象占用的内存被释放时,WeakReference对象就被添加到它的ReferenceQueue(如果后者存在的话) 

        PhantomReference必须等待finalize执行完成才可以enqueue。这个正如主贴所说: 而PhantomReference当GC对referent的状态改变时,在把PhantomReference放入ReferenceQueue之前referent已经被GC处理到Reclaimed了,即该referent被销毁了。 

 

文章来源:http://zhang-xzhi-xjtu.iteye.com/blog/413159

猜你喜欢

转载自bijian1013.iteye.com/blog/2289781