深入理解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的順序問題。
按照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只能執行一次
物件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的處理。
分析,在GC執行時,檢測到new A()生成的物件只有一個WeakReference引用著,所以決定回收它,首先clear WeakReference的referent,然後referent的狀態為finalizable,同時或者一段時間後把WeakReference放入監聽的ReferenceQueue中。
注意有時候最後的Assert.assertNotNull(obj);有時會失敗,因為還沒有來的及把WeakReference放入監聽的ReferenceQueue中。
換成PhantomReference試試,
貌似和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已經被銷燬了。
即使new A()出來的物件再生了,在queue中還是可以看到WeakReference。
第一次gc後,由於new A()的物件再生了,所以queue是空的,因為物件沒有銷燬。
當第二次gc後,new A()的物件銷燬以後,在queue中才可以看到PhantomReference。
所以PhantomReference可以更精細的對物件生命週期進行監控。
Q&A
Q1:有這樣一個問題,為什麼UT會Fail?不是說物件會重生嗎,到底哪裡有問題?
A: 物件是會重生不錯。
這裡會Fail有兩個可能的原因,一個是gc的行為是不確定的,沒有什麼會保證gc執行。呵呵,我承認,我在console上看到東西了,所以我知道gc執行了一次。
另一個問題是gc的執行緒和我們跑ut的執行緒是兩個獨立的執行緒。即使gc執行緒裡物件重生了,很有可能是我們跑完ut之後的事情了。這裡就是時序問題了。
這個ut和上面那個大同小異。
一般情況下,code執行到這裡,gc的物件重生應該還沒有發生。所以我們下面的斷言有很大的概論是成立的。
讓ut的執行緒睡眠5秒,嗯,gc的執行緒有可能已經執行完物件重生了。所以下面這行有可能通過測試。
嗯,測試通過。但是沒有人確保它每次都通過。所以我兩處的註釋都宣告有可能fail。
這個例子很好的說明了如何在程式中用gc和重生的基本原則。
依賴gc會引入一些不確定的行為。
重生會導致不確定以及有可能的時序問題。
所以一般我們不應該使用gc和重生,但是能深入的理解這些概念又對我們程式設計有好處。
這兩個測試如果作為一個TestSuite跑的話,情況又會有不同。因為第一個測試失敗之後和第二個測試執行之間,gc執行了物件重生。如此,以下斷言失敗的概率會升高。
To luliruj and DLevin
首先謝謝你們的回覆,這個帖子發了好久了,竟然還有人回覆。
reclaimed的問題可以參看本帖上邊的URL。
關於finalize和ReferenceQueue和關係,主貼已經解釋了,luliruj給出了不同的解釋。
這個地方我們可以用小程式驗證一下.
在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被銷燬了。
概述
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的順序問題。
- 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只能執行一次
- 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的處理。
- 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);
注意有時候最後的Assert.assertNotNull(obj);有時會失敗,因為還沒有來的及把WeakReference放入監聽的ReferenceQueue中。
換成PhantomReference試試,
- 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);
而PhantomReference當GC對referent的狀態改變時,在把PhantomReference放入ReferenceQueue之前referent已經被GC處理到Reclaimed了,即該referent被銷燬了。
搞了這麼多,有什麼用?可以使用PhantomReference更好的控制一些關於物件生命週期的事情,當WeakReference放入ReferenceQueue時,並不能保證該referent是被銷燬了。別忘了物件可以在finalize方法裡再生。而使用PhantomReference,當在ReferenceQueue中發現PhantomReference時,可以保證referent已經被銷燬了。
- 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);
- 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中才可以看到PhantomReference。
所以PhantomReference可以更精細的對物件生命週期進行監控。
Q&A
Q1:有這樣一個問題,為什麼UT會Fail?不是說物件會重生嗎,到底哪裡有問題?
- 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之後的事情了。這裡就是時序問題了。
- 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的物件重生應該還沒有發生。所以我們下面的斷言有很大的概論是成立的。
- // 有可能fail.
- Assert.assertNull(Test.t);
讓ut的執行緒睡眠5秒,嗯,gc的執行緒有可能已經執行完物件重生了。所以下面這行有可能通過測試。
- Assert.assertNotNull(Test.t);
這個例子很好的說明了如何在程式中用gc和重生的基本原則。
依賴gc會引入一些不確定的行為。
重生會導致不確定以及有可能的時序問題。
所以一般我們不應該使用gc和重生,但是能深入的理解這些概念又對我們程式設計有好處。
這兩個測試如果作為一個TestSuite跑的話,情況又會有不同。因為第一個測試失敗之後和第二個測試執行之間,gc執行了物件重生。如此,以下斷言失敗的概率會升高。
- // 有可能fail.
- Assert.assertNull(Test.t);
To luliruj and DLevin
首先謝謝你們的回覆,這個帖子發了好久了,竟然還有人回覆。
reclaimed的問題可以參看本帖上邊的URL。
關於finalize和ReferenceQueue和關係,主貼已經解釋了,luliruj給出了不同的解釋。
這個地方我們可以用小程式驗證一下.
- 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被銷燬了。
相關文章
- GC和解構函式(Finalize 方法)GC函式
- JVM相關 - 深入理解 System.gc()JVMGC
- final、finally、finalize的理解
- 深入理解jvm記憶體模型以及gc原理JVM記憶體模型GC
- 深入理解JVM-記憶體模型(jmm)和GCJVM記憶體模型GC
- 【譯】深入理解G1的GC日誌(一)GC
- 深入理解Java的垃圾回收機制(GC)實現原理JavaGC
- 深入理解JVM虛擬機器-物件引用,GC與記憶體分配回收JVM虛擬機物件GC記憶體
- finalize方法
- eclipse設定檢視GC日誌和如何理解GC日誌EclipseGC
- 淺談對java-GC的理解JavaGC
- final和finalize
- final:finalize:finally:
- Go記憶體分配和GC的理解Go記憶體GC
- 深入理解Java虛擬機器筆記之五GC日誌和垃圾收集器引數Java虛擬機筆記GC
- 深入理解margin
- 深入理解ReactReact
- 深入理解KVO
- 深入理解 ReentrantLockReentrantLock
- 深入理解 PWA
- 深入理解BFC
- 深入理解volatile
- 深入理解MVCMVC
- 深入理解 TypeScriptTypeScript
- 深入理解JSCoreJS
- 深入理解JavaScriptCoreJavaScript
- 深入理解Isolate
- 深入理解 JVMJVM
- 深入理解HashMapHashMap
- 深入理解ThreadLocalthread
- 深入理解TransformORM
- 深入理解 GitGit
- 深入理解reduxRedux
- BFC深入理解
- 深入理解paddingpadding
- 深入理解JSXJS
- 深入理解 SynchronizationContextContext
- 深入理解JVMJVM
- 深入理解AQSAQS