深入理解ReferenceQueue GC finalize Reference

 

转载自 ---- http://zhang-xzhi-xjtu.iteye.com/blog/413159

 

目录

概述
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代码   收藏代码
  1. public   class  A {  
  2.     B b;  
  3.     public   void  finalize() {  
  4.         System.out.println("method A.finalize at "  + System.nanoTime());  
  5.     }  
  6. }  
  7.   
  8. public   class  B {  
  9.     public   void  finalize() {  
  10.         System.out.println("method B.finalize at "  + System.nanoTime());  
  11.     }  
  12. }  
  13.   
  14.     A a = new  A();  
  15.     a.b = new  B();  
  16.     a = null ;  
  17.     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代码   收藏代码
  1. public   class  B {  
  2.   
  3.     static  B b;  
  4.   
  5.     public   void  finalize() {  
  6.         System.out.println("method B.finalize" );  
  7.         b = this ;  
  8.     }  
  9. }  
  10.   
  11.     B b = new  B();  
  12.     b = null ;  
  13.     System.gc();  
  14.     B.b = null ;  
  15.     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代码   收藏代码
  1. public   class  A {  
  2. }  
  3.   
  4.     ReferenceQueue queue = new  ReferenceQueue();  
  5.     WeakReference ref = new  WeakReference( new  A(), queue);  
  6.     Assert.assertNotNull(ref.get());  
  7.   
  8.     Object obj = null ;  
  9.     obj = queue.poll();  
  10.     Assert.assertNull(obj);  
  11.   
  12.     System.gc();  
  13.   
  14.     Assert.assertNull(ref.get());  
  15.     obj = queue.poll();  
  16.     Assert.assertNotNull(obj);  



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

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

换成PhantomReference试试,

Java代码   收藏代码
  1. ReferenceQueue queue =  new  ReferenceQueue();  
  2. PhantomReference ref = new  PhantomReference( new  A(), queue);  
  3.   
  4. Assert.assertNull(ref.get());  
  5.   
  6. Object obj = null ;  
  7. obj = queue.poll();  
  8.   
  9. Assert.assertNull(obj);  
  10.   
  11. System.gc();  
  12.   
  13. Thread.sleep(10000 );  
  14.   
  15. System.gc();  
  16.   
  17. Assert.assertNull(ref.get());  
  18. obj = queue.poll();  
  19. 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代码   收藏代码
  1. public   class  A {  
  2.     static  A a;  
  3.     public   void  finalize() {  
  4.         a = this ;  
  5.     }  
  6. }  
  7.   
  8.     ReferenceQueue queue = new  ReferenceQueue();  
  9.   
  10.     WeakReference ref = new  WeakReference( new  A(), queue);  
  11.   
  12.     Assert.assertNotNull(ref.get());  
  13.   
  14.     Object obj = null ;  
  15.   
  16.     obj = queue.poll();  
  17.   
  18.     Assert.assertNull(obj);  
  19.   
  20.     System.gc();  
  21.   
  22.     Thread.sleep(10000 );  
  23.   
  24.     System.gc();  
  25.   
  26.     Assert.assertNull(ref.get());  
  27.   
  28.     obj = queue.poll();  
  29.   
  30.     Assert.assertNotNull(obj);  



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

Java代码   收藏代码
  1. ReferenceQueue queue =  new  ReferenceQueue();  
  2.   
  3. PhantomReference ref = new  PhantomReference( new  A(), queue);  
  4.   
  5. Assert.assertNull(ref.get());  
  6.   
  7. Object obj = null ;  
  8.   
  9. obj = queue.poll();  
  10.   
  11. Assert.assertNull(obj);  
  12.   
  13. // 第一次gc   
  14.   
  15. System.gc();  
  16.   
  17. Thread.sleep(10000 );  
  18.   
  19. System.gc();  
  20.   
  21. Assert.assertNull(ref.get());  
  22.   
  23. obj = queue.poll();  
  24.   
  25. Assert.assertNull(obj);  
  26.   
  27. A.a = null ;  
  28.   
  29. // 第二次gc   
  30.   
  31. System.gc();  
  32.   
  33. obj = queue.poll();  
  34.   
  35. Assert.assertNotNull(obj);  



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

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

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


Q&A

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

Java代码   收藏代码
  1. public   class  Test {  
  2.   
  3.     static  Test t;  
  4.   
  5.     @Override   
  6.     protected   void  finalize() {  
  7.         System.out.println("finalize" );  
  8.         t = this ;  
  9.     }  
  10. }  
  11.   
  12.     public   void  testFinalize() {  
  13.         Test t = new  Test();  
  14.         Assert.assertNotNull(t);  
  15.         t = null ;  
  16.         System.gc();  
  17.         Assert.assertNull(t);  
  18.         Assert.assertNotNull(Test.t);  
  19.     }  



A: 对象是会重生不错。
这里会Fail有两个可能的原因,一个是gc的行为是不确定的,没有什么会保证gc运行。呵呵,我承认,我在console上看到东西了,所以我知道gc运行了一次。
另一个问题是gc的线程和我们跑ut的线程是两个独立的线程。即使gc线程里对象重生了,很有可能是我们跑完ut之后的事情了。这里就是时序问题了。

Java代码   收藏代码
  1. public   void  testFinalize()  throws  Exception {  
  2.     Test t = new  Test();  
  3.     Assert.assertNotNull(t);  
  4.     t = null ;  
  5.     System.gc();  
  6.     Assert.assertNull(t);  
  7.   
  8.     // 有可能fail.   
  9.     Assert.assertNull(Test.t);  
  10.     // 等一下gc,让gc线程的对象重生执行完。   
  11.     Thread.sleep(5000 );  
  12.     // 有可能fail.   
  13.     Assert.assertNotNull(Test.t);  
  14. }  


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

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

Java代码   收藏代码
  1. // 有可能fail.   
  2. Assert.assertNull(Test.t);  


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

Java代码   收藏代码
  1. Assert.assertNotNull(Test.t);  



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

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

Java代码   收藏代码
  1. // 有可能fail.   
  2. Assert.assertNull(Test.t); 

你可能感兴趣的:(jvm,GC)