深入理解ReferenceQueue GC finalize Reference

idaretobe發表於2015-01-14
目錄 
概述 
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);  


To luliruj and DLevin 
首先謝謝你們的回覆,這個帖子發了好久了,竟然還有人回覆。 
reclaimed的問題可以參看本帖上邊的URL。 
關於finalize和ReferenceQueue和關係,主貼已經解釋了,luliruj給出了不同的解釋。 
這個地方我們可以用小程式驗證一下. 
Java程式碼  收藏程式碼
  1. public class Tem {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.   
  5.         ReferenceQueue queue = new ReferenceQueue();  
  6.         // SoftReference ref = new SoftReference(new B(), queue);  
  7.         // WeakReference ref = new WeakReference(new B(), queue);  
  8.         PhantomReference ref = new PhantomReference(new B(), queue);  
  9.         while (true) {  
  10.             Object obj = queue.poll();  
  11.             if (obj != null) {  
  12.                 System.out.println("queue.poll at " + new Date() + " " + obj);  
  13.                 break;  
  14.             }  
  15.             System.gc();  
  16.             System.out.println("run once.");  
  17.         }  
  18.   
  19.         Thread.sleep(100000);  
  20.     }  
  21.   
  22. }  
  23.   
  24. class B {  
  25.   
  26.     @Override  
  27.     protected void finalize() throws Throwable {  
  28.         System.out.println("finalize at " + new Date());  
  29.     }  
  30. }  

在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被銷燬了。 

相關文章