深入理解ReferenceQueue GC finalize Reference

概述 
1 先看一个对象finalize的顺序问题。 
2 对象再生及finalize只能执行一次 
3 SoftReference WeakReference 
4 PhantomReference 
5 ReferenceQueue 
Q&A 


概述 

先说一些基本的东西,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的顺序问题。 

Java代码  
public class A {  
    B b;  
    public void finalize() {  
        System.out.println("method A.finalize at " + System.nanoTime());  
    }  
}  
  
public class B {  
    public void finalize() {  
        System.out.println("method B.finalize at " + System.nanoTime());  
    }  
}  
  
    A a = new A();  
    a.b = new B();  
    a = null;  
    System.gc();  


按照http://java.sun.com/developer/technicalArticles/javase/finalization/ 
所说,对象a在finalize之前会保持b的引用,但是实验中对象a和a中的对象b的finalize方法运行时间有先有后,而且大部分时间里,a的finalize方法的执行时间是晚于b的finalize方法的。我记着java编程语言书中说是一切可以finalize的对象的finalize方法的执行顺序是不确定的。到底应该听谁的?最好的实践就是不要依赖finalize的顺序或者写一些防御代码。 

【note】我仍然坚持最好的实践就是不要依赖finalize的顺序或者写一些防御代码。但是通过进一步的学习和实验,因为a有可能复活,所以在a没有决定到底复活不复活之前b是不会被回收的。控制台的顺序问题应该是多线程的问题导致的。 
【note】查看了JLS后,确定了finalize是乱序执行的。 

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

Java代码  
public class B {  
  
    static B b;  
  
    public void finalize() {  
        System.out.println("method B.finalize");  
        b = this;  
    }  
}  
  
    B b = new B();  
    b = null;  
    System.gc();  
    B.b = null;  
    System.gc();  

对象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的处理。 

Java代码  
public class A {  
}  
  
    ReferenceQueue queue = new ReferenceQueue();  
    WeakReference ref = new WeakReference(new A(), queue);  
    Assert.assertNotNull(ref.get());  
  
    Object obj = null;  
    obj = queue.poll();  
    Assert.assertNull(obj);  
  
    System.gc();  
  
    Assert.assertNull(ref.get());  
    obj = queue.poll();  
    Assert.assertNotNull(obj);  


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

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

换成PhantomReference试试, 

Java代码  
ReferenceQueue queue = new ReferenceQueue();  
PhantomReference ref = new PhantomReference(new A(), queue);  
  
Assert.assertNull(ref.get());  
  
Object obj = null;  
obj = queue.poll();  
  
Assert.assertNull(obj);  
  
System.gc();  
  
Thread.sleep(10000);  
  
System.gc();  
  
Assert.assertNull(ref.get());  
obj = queue.poll();  
Assert.assertNotNull(obj);  


貌似和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已经被销毁了。 

Java代码  
public class A {  
    static A a;  
    public void finalize() {  
        a = this;  
    }  
}  
  
    ReferenceQueue queue = new ReferenceQueue();  
  
    WeakReference ref = new WeakReference(new A(), queue);  
  
    Assert.assertNotNull(ref.get());  
  
    Object obj = null;  
  
    obj = queue.poll();  
  
    Assert.assertNull(obj);  
  
    System.gc();  
  
    Thread.sleep(10000);  
  
    System.gc();  
  
    Assert.assertNull(ref.get());  
  
    obj = queue.poll();  
  
    Assert.assertNotNull(obj);  


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

Java代码  
ReferenceQueue queue = new ReferenceQueue();  
  
PhantomReference ref = new PhantomReference(new A(), queue);  
  
Assert.assertNull(ref.get());  
  
Object obj = null;  
  
obj = queue.poll();  
  
Assert.assertNull(obj);  
  
// 第一次gc  
  
System.gc();  
  
Thread.sleep(10000);  
  
System.gc();  
  
Assert.assertNull(ref.get());  
  
obj = queue.poll();  
  
Assert.assertNull(obj);  
  
A.a = null;  
  
// 第二次gc  
  
System.gc();  
  
obj = queue.poll();  
  
Assert.assertNotNull(obj);  


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

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

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


Q&A 

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

Java代码  
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之后的事情了。这里就是时序问题了。 

Java代码  
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的对象重生应该还没有发生。所以我们下面的断言有很大的概论是成立的。 
Java代码  
// 有可能fail.  
Assert.assertNull(Test.t);  

让ut的线程睡眠5秒,嗯,gc的线程有可能已经执行完对象重生了。所以下面这行有可能通过测试。 
Java代码  
Assert.assertNotNull(Test.t);  


嗯,测试通过。但是没有人确保它每次都通过。所以我两处的注释都声明有可能fail。 
这个例子很好的说明了如何在程序中用gc和重生的基本原则。 
依赖gc会引入一些不确定的行为。 
重生会导致不确定以及有可能的时序问题。 
所以一般我们不应该使用gc和重生,但是能深入的理解这些概念又对我们编程有好处。 

这两个测试如果作为一个TestSuite跑的话,情况又会有不同。因为第一个测试失败之后和第二个测试执行之间,gc执行了对象重生。如此,以下断言失败的概率会升高。 
Java代码  
// 有可能fail.  
Assert.assertNull(Test.t);  


To luliruj and DLevin 
首先谢谢你们的回复,这个帖子发了好久了,竟然还有人回复。 
reclaimed的问题可以参看本帖上边的URL。 
关于finalize和ReferenceQueue和关系,主贴已经解释了,luliruj给出了不同的解释。 
这个地方我们可以用小程序验证一下. 
Java代码  
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的。这个就否掉了luliruj所说的 

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

PhantomReference必须等待finalize执行完成才可以enqueue。 
这个正如主贴所说: 
而PhantomReference当GC对referent的状态改变时,在把PhantomReference放入ReferenceQueue之前referent已经被GC处理到Reclaimed了,即该referent被销毁了。 
9 
顶0 
踩分享到:  
finally两例 | 统计代码的小工具CodeLineCounter2.0
2009-06-22 22:55浏览 4186评论(8)分类:编程语言相关推荐
评论
8 楼 zhang_xzhi_xjtu 2012-06-27  
答复我更新到主贴的最后了。
7 楼 luliruj 2012-06-25  
DLevin 写道
zhang_xzhi_xjtu 写道
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。 

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。

JDK文档中给出关于PhantomReference的解释是:
Unlike soft and weak references, phantom references are not automatically cleared by the garbage collector as they are enqueued. An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.
这样的描述,我的理解是在PhantomReference被Enqueue之后,它到目标引用还是存在的,要手动clear,虽然用get方法返回的还是null。这样是否表明目标引用还没有被回收。不知道你所知的reclaimed是指什么意思?而且在这一点上,我们如何做清理工作呢?我始终想不明白PhantomReference和finalizer机制是如何表现它的优势的,更直接一些,PhantomReference是如何使用的我也不是很了解,是否有人知道帮忙解释一些,谢谢。

我觉得LZ的对什么时候WeekReference和PhantomReference的解释是有问题的,与JDK文档也是相违背的,个人更倾向于以下的观点:
  垃圾收集器每次运行时都可以随意地释放不再是强可及的对象占用的内存。如果垃圾收集器发现了软可及对象,就会出现下列情况:
  (1)SoftReference 对象的 referent 域被设置为 null ,从而使该对象不再引用 heap 对象。
  (2)SoftReference 引用过的 heap 对象被声明为 finalizable 。
  (3)当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放, SoftReference 对象就被添加到它的 ReferenceQueue (如果后者存在的话)。
  如果垃圾收集器发现了弱可及对象,就会出现下列情况:
  (1)WeakReference 对象的 referent 域被设置为 null ,从而使该对象不再引用 heap 对象。
  (2)WeakReference 引用过的 heap 对象被声明为 finalizable 。
  (3)当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放时, WeakReference 对象就被添加到它的 ReferenceQueue (如果后者存在的话)。
  如果垃圾收集器发现了虚可及对象,就会出现下列情况:
  (1)PhantomReference 引用过的 heap 对象被声明为 finalizable 。
(2)与软引用和弱引用有所不同, PhantomReference 在堆对象被释放之前就被添加到它的 ReferenceQueue 。(请记住,所有的 PhantomReference 对象都必须用经过关联的 ReferenceQueue 来创建。)这使您能够在堆对象被回收之前采取行动。
以上摘自IBM上的一篇文章:http://www.ibm.com/developerworks/cn/java/j-refs/
6 楼 DLevin 2010-10-15  
zhang_xzhi_xjtu 写道
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。 

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。

JDK文档中给出关于PhantomReference的解释是:
Unlike soft and weak references, phantom references are not automatically cleared by the garbage collector as they are enqueued. An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.
这样的描述,我的理解是在PhantomReference被Enqueue之后,它到目标引用还是存在的,要手动clear,虽然用get方法返回的还是null。这样是否表明目标引用还没有被回收。不知道你所知的reclaimed是指什么意思?而且在这一点上,我们如何做清理工作呢?我始终想不明白PhantomReference和finalizer机制是如何表现它的优势的,更直接一些,PhantomReference是如何使用的我也不是很了解,是否有人知道帮忙解释一些,谢谢。
5 楼 zhang_xzhi_xjtu 2010-07-20  
可以参考我的这个博客
http://zhang-xzhi-xjtu.iteye.com/blog/484934
讲解的更详细。
对象不会被不停地被gc(这个的gc指的是执行finalize方法)。

hfhwan 写道
zhang_xzhi_xjtu 写道
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。 

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。


我想问一下,gc是如何保证WeakReference是Finalizable,PhantomReference是reclaimed。

我看到有的文章说WeakReference是在fnalize执行之后,对象被销毁之后放入referenceQueue的。
而PhantomReference是在finalize执行之后,对象被销毁之前放入refrenceQueue的。

如果都是在finalize执行之后,那如你文章中说到的,在finalize函数中使其复活后,这个对象难道还是会被标识成Finalizable,从而被放到referenceQueue中。
这样的话不是会引起该对象不停地被gc?

4 楼 hfhwan 2010-07-20  
zhang_xzhi_xjtu 写道
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。 

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。


我想问一下,gc是如何保证WeakReference是Finalizable,PhantomReference是reclaimed。

我看到有的文章说WeakReference是在fnalize执行之后,对象被销毁之后放入referenceQueue的。
而PhantomReference是在finalize执行之后,对象被销毁之前放入refrenceQueue的。

如果都是在finalize执行之后,那如你文章中说到的,在finalize函数中使其复活后,这个对象难道还是会被标识成Finalizable,从而被放到referenceQueue中。
这样的话不是会引起该对象不停地被gc?
3 楼 erlengleng 2009-12-10  
好文章,感谢楼主分享
2 楼 zhang_xzhi_xjtu 2009-09-19  
引入ReferenceQueue是为了更好的监视对象回收的时机,同时可以做一些自定义的动作。
这里WeakReference和PhantomReference看似一样,但是实际还是有些不一样的。

一个对象生命周期为,正常,Finalizable ,Finalized,Reclaimed。 

Enqueue之前,对WeakReference,GC保证它是Finalizable,但是有可能在执行这个对象的finalize方法时对象重生。所以监视到一个WeakReference对象enqueue并不能保证该对象已经被确定性销毁。

Enqueue之前,对PhantomReference,GC保证它是reclaimed,就是说该对象已经被认为要被确定性销毁了,没有任何机会重生了。所以我们可以在这个点做一些必须保证对象被销毁才适合做的清理工作。
1 楼 langyu 2009-09-18  
1.PhantomReference的目标只为了知道referent什么时候被回收了?
2.把Reference放入ReferenceQueue是有什么具体目的呢?能解决什么问题。
盼LZ明示,谢谢。


你可能感兴趣的:(java,se)