分享

深入理解ReferenceQueue GC finalize Reference

 Baruch 2017-09-08
  1. 概述   

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

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

  4. 3 SoftReference WeakReference   

  5. 4 PhantomReference   

  6. 5 ReferenceQueue   

  7. Q&A   

  8. 概述   

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

  10. 一个正常的对象的生命周期。   

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

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

  13. Reference中引用的object叫做referent。   

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

  15. Java代码    

  16. public class A {    

  17.     B b;    

  18.     public void finalize() {    

  19.         System.out.println("method A.finalize at " + System.nanoTime());    

  20.     }    

  21. }    

  22. public class B {    

  23.     public void finalize() {    

  24.         System.out.println("method B.finalize at " + System.nanoTime());    

  25.     }    

  26. }    

  27.     A a = new A();    

  28.     a.b = new B();    

  29.     a = null;    

  30.     System.gc();    

  31. 按照http://java./developer/technicalArticles/javase/finalization/   

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

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

  34. 【note】查看了JLS后,确定了finalize是乱序执行的。   

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

  36. Java代码    

  37. public class B {    

  38.     static B b;    

  39.     public void finalize() {    

  40.         System.out.println("method B.finalize");    

  41.         b = this;    

  42.     }    

  43. }    

  44.     B b = new B();    

  45.     b = null;    

  46.     System.gc();    

  47.     B.b = null;    

  48.     System.gc();    

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

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

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

  52. 3 SoftReference WeakReference   

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

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

  55. 4 PhantomReference   

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

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

  58. 5 ReferenceQueue   

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

  60. Java代码    

  61. public class A {    

  62. }    

  63.     ReferenceQueue queue = new ReferenceQueue();    

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

  65.     Assert.assertNotNull(ref.get());    

  66.     Object obj = null;    

  67.     obj = queue.poll();    

  68.     Assert.assertNull(obj);    

  69.     System.gc();    

  70.     Assert.assertNull(ref.get());    

  71.     obj = queue.poll();    

  72.     Assert.assertNotNull(obj);    

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

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

  75. 换成PhantomReference试试,   

  76. Java代码    

  77. ReferenceQueue queue = new ReferenceQueue();    

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

  79. Assert.assertNull(ref.get());    

  80. Object obj = null;    

  81. obj = queue.poll();    

  82. Assert.assertNull(obj);    

  83. System.gc();    

  84. Thread.sleep(10000);    

  85. System.gc();    

  86. Assert.assertNull(ref.get());    

  87. obj = queue.poll();    

  88. Assert.assertNotNull(obj);    

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

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

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

  92. Java代码    

  93. public class A {    

  94.     static A a;    

  95.     public void finalize() {    

  96.         a = this;    

  97.     }    

  98. }    

  99.     ReferenceQueue queue = new ReferenceQueue();    

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

  101.     Assert.assertNotNull(ref.get());    

  102.     Object obj = null;    

  103.     obj = queue.poll();    

  104.     Assert.assertNull(obj);    

  105.     System.gc();    

  106.     Thread.sleep(10000);    

  107.     System.gc();    

  108.     Assert.assertNull(ref.get());    

  109.     obj = queue.poll();    

  110.     Assert.assertNotNull(obj);    

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

  112. Java代码    

  113. ReferenceQueue queue = new ReferenceQueue();    

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

  115. Assert.assertNull(ref.get());    

  116. Object obj = null;    

  117. obj = queue.poll();    

  118. Assert.assertNull(obj);    

  119. // 第一次gc    

  120. System.gc();    

  121. Thread.sleep(10000);    

  122. System.gc();    

  123. Assert.assertNull(ref.get());    

  124. obj = queue.poll();    

  125. Assert.assertNull(obj);    

  126. A.a = null;    

  127. // 第二次gc    

  128. System.gc();    

  129. obj = queue.poll();    

  130. Assert.assertNotNull(obj);    

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

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

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

  134. Q&A   

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

  136. Java代码    

  137. public class Test {    

  138.     static Test t;    

  139.     @Override    

  140.     protected void finalize() {    

  141.         System.out.println("finalize");    

  142.         t = this;    

  143.     }    

  144. }    

  145.     public void testFinalize() {    

  146.         Test t = new Test();    

  147.         Assert.assertNotNull(t);    

  148.         t = null;    

  149.         System.gc();    

  150.         Assert.assertNull(t);    

  151.         Assert.assertNotNull(Test.t);    

  152.     }    

  153. A: 对象是会重生不错。   

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

  155. 另一个问题是gc的线程和我们跑ut的线程是两个独立的线程。即使gc线程里对象重生了,很有可能是我们跑完ut之后的事情了。这里就是时序问题了。   

  156. Java代码    

  157. public void testFinalize() throws Exception {    

  158.     Test t = new Test();    

  159.     Assert.assertNotNull(t);    

  160.     t = null;    

  161.     System.gc();    

  162.     Assert.assertNull(t);    

  163.     // 有可能fail.    

  164.     Assert.assertNull(Test.t);    

  165.     // 等一下gc,让gc线程的对象重生执行完。    

  166.     Thread.sleep(5000);    

  167.     // 有可能fail.    

  168.     Assert.assertNotNull(Test.t);    

  169. }    

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

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

  172. Java代码    

  173. // 有可能fail.    

  174. Assert.assertNull(Test.t);    

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

  176. Java代码    

  177. Assert.assertNotNull(Test.t);    

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

  179. 这个例子很好的说明了如何在程序中用gc和重生的基本原则。   

  180. 依赖gc会引入一些不确定的行为。   

  181. 重生会导致不确定以及有可能的时序问题。   

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

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

  184. Java代码    

  185. // 有可能fail.    

  186. Assert.assertNull(Test.t);    

  187. To luliruj and DLevin   

  188. 首先谢谢你们的回复,这个帖子发了好久了,竟然还有人回复。   

  189. reclaimed的问题可以参看本帖上边的URL。   

  190. 关于finalize和ReferenceQueue和关系,主贴已经解释了,luliruj给出了不同的解释。   

  191. 这个地方我们可以用小程序验证一下.   

  192. Java代码    

  193. public class Tem {    

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

  195.         ReferenceQueue queue = new ReferenceQueue();    

  196.         // SoftReference ref = new SoftReference(new B(), queue);    

  197.         // WeakReference ref = new WeakReference(new B(), queue);    

  198.         PhantomReference ref = new PhantomReference(new B(), queue);    

  199.         while (true) {    

  200.             Object obj = queue.poll();    

  201.             if (obj != null) {    

  202.                 System.out.println("queue.poll at " + new Date() + " " + obj);    

  203.                 break;    

  204.             }    

  205.             System.gc();    

  206.             System.out.println("run once.");    

  207.         }    

  208.         Thread.sleep(100000);    

  209.     }    

  210. }    

  211. class B {    

  212.     @Override    

  213.     protected void finalize() throws Throwable {    

  214.         System.out.println("finalize at " + new Date());    

  215.     }    

  216. }    

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

  218. SoftReference/WeakReference都是不需要finalize执行就可以enqueue的。这个就否掉了luliruj所说的   

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

  220. PhantomReference必须等待finalize执行完成才可以enqueue。   

  221. 这个正如主贴所说:   

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多