深入理解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明示,謝謝。

發佈了22 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章