12 垃圾回收相關概念

coder1qiang發表於2020-11-10

1. System.gc()的理解

  • 在預設情況下,通過System.gc();或者Runtime.getRuntime().gc();的呼叫,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。然而System.gc();的呼叫附帶一個免責宣告,即無法保證對垃圾收集器的呼叫(不能確保垃圾收集行為立即生效)。
  • JVM實現者可以通過System.gc();的呼叫來決定JVM的GC行為。一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在編寫一個效能基準測試,我們可以在執行之間呼叫System.gc();
package com.java.gc1;

/**
 * @author rrqstart
 * @Description 測試 System.gc();是否一定會執行GC行為
 */
public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();

        // 提醒JVM的垃圾回收器執行GC,但是不確定是否馬上執行GC
        System.gc(); // System.gc();與Runtime.getRuntime().gc();的作用一樣

//        System.runFinalization(); // 強制呼叫失去引用的物件的finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest類重寫的finalize()方法!");
        //如果執行了GC,在回收某物件之前會呼叫該物件的finalize()方法
    }
}
package java.lang;

public final class System {
	//......
	public static void gc() {
        Runtime.getRuntime().gc();
    }
    //......
}
package java.lang;

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

	private Runtime() {}
    //......
    public native void gc();
}
package com.java.gc1;

/**
 * @author rrqstart
 * @Description 手動GC來理解不可達物件的回收
 * VM Args : -XX:+PrintGCDetails
 */
public class LocalVarGC {
    public void localvarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024]; //10MB
        System.gc();
        //觸發YGC沒有回收buffer對應的物件實體,然後再觸發Full GC將buffer對應的物件實體存入Old區。
    }

    public void localvarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
        //觸發YoungGC的時候,buffer對應的物件實體已經被回收了。
    }

    public void localvarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
        //buffer對應的物件實體在YGC時不會被回收,在後續的Full GC將其存放到Old區。在GC時,buffer還存放在區域性變數表索引為1的槽中(通過檢視位元組碼可以看到區域性變數表的長度為2)。
    }

    public void localvarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
        //buffer對應的物件實體會被回收,因為它之前存放在區域性變數表索引為1的槽中,但是後面定義的value把這個槽重新佔用了,替換了buffer,導致沒有了指向buffer對應的物件實體的引用了。
    }

    public void localvarGC5() {
        localvarGC1();
        System.gc();
        //localvarGC1()方法中buffer對應的物件實體會在localvarGC5()方法中的這次GC之後被回收。
    }

    public static void main(String[] args) {
        LocalVarGC local = new LocalVarGC();
//        local.localvarGC1();
//        local.localvarGC2();
//        local.localvarGC3();
//        local.localvarGC4();
        local.localvarGC5();
    }
}

2. 記憶體溢位與記憶體洩漏

2.1 記憶體溢位

  • 記憶體溢位相對於記憶體洩漏來說,儘管更容易被理解,但是同樣的,記憶體溢位也是引發程式崩潰的罪魁禍首之一。
  • 由於GC一直在發展,一般情況下,除非應用程式佔用的記憶體增長速度非常快,造成垃圾回收已經跟不上記憶體消耗的速度,否則不太容易出現OOM的情況。
  • 大多數情況下,GC會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨佔式的Full GC操作,這時候會回收大量的記憶體,供應用程式繼續使用。
  • javadoc中對OutOfMemoryError的解釋是:沒有空閒記憶體,並且垃圾收集器也無法提供更多記憶體。
  • 首先說沒有空閒記憶體的情況:說明Java虛擬機器的堆記憶體不夠。原因有二:
    • Java虛擬機器的堆記憶體設定不夠。比如:可能存在記憶體洩漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯式指定JVM堆大小或者指定數值偏小。我們可以通過引數-Xms-Xmx來調整。
    • 程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)。對於老版本的Oracle JDK,因為永久代的大小是有限的,並且JVM對永久代垃圾回收非常不積極(如:常量池回收、解除安裝不再需要的型別),所以當我們不斷新增新型別的時候,永久代出現OutOfMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間,也會導致OOM問題。對應的異常資訊,會標記出來和永久代相關:“java.lang.OutOfMemoryError:PermGen space"。隨著後設資料區的引入,方法區記憶體已經不再那麼窘迫,所以相應的OOM有所改觀,出現OOM,異常資訊則變成了:“java.lang.OutofMemoryError:Metaspace"。即直接記憶體不足,也會導致OOM。
  • 這裡面隱含著一層意思是,在丟擲OutOfMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。
    • 例如:在引用機制分析中,涉及到JVM會去嘗試回收軟引用指向的物件等。
    • 在 java.nio.BIts.reserveMemory() 方法中,我們能清楚的看到,System.gc() 會被呼叫,以清理空間。
  • 當然,也不是在任何情況下垃圾收集器都會被觸發的。比如,我們去分配一個超大物件,類似一個超大陣列超過堆的最大值,JVM可以判斷出垃圾收集並不能解決這個問題,所以直接丟擲OutOfMemoryError。

2.2 記憶體洩漏

  • 記憶體洩漏(Memory Leak)也稱作“儲存滲漏”。嚴格來說,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體洩漏。
  • 但實際上很多時候一些不太好的實踐(或疏忽)會導致物件的生命週期變得很長甚至導致OOM,也可以叫做寬泛意義上的“記憶體洩漏”
  • 儘管記憶體洩漏並不會立刻引起程式崩潰,但是一旦發生記憶體洩漏,程式中的可用記憶體就會被逐步蠶食,直至耗盡所有記憶體,最終出現OutOfMemoryError異常,導致程式崩潰。
  • 注意,這裡的儲存空間並不是指實體記憶體,而是指虛擬記憶體大小,這個虛擬記憶體大小取決於磁碟交換區設定的大小。

圖12-1 記憶體洩漏
  • 記憶體洩漏的例子:
    • 單例模式:單例的生命週期和應用程式是一樣長的,所以單例程式中,如果持有對外部物件的引用的話,那麼這個外部物件是不能被回收的,則會導致記憶體洩漏的產生。
    • 一些提供close的資源未關閉導致記憶體洩漏:資料庫連線、網路連線和IO連線必須手動close,否則是不能被回收的。

3. Stop The World

  • Stop The World,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒(主要指使用者執行緒)都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。
  • 可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。
    • 分析工作必須在一個能確保一致性的快照中進行。
    • 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上。
    • 如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證。
  • 被STW中斷的應用程式執行緒會在完成GC之後恢復,頻繁中斷會讓使用者感覺像是網速不快造成電影卡帶一樣,所以我們需要減少STW的發生。
  • STW事件和採用哪款GC無關,所有的GC都有這個事件。
  • 哪怕是G1也不能完全避免Stop-the-World情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,儘可能地縮短了暫停時間。
  • STW是JVM在後臺自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。
  • 開發中不要用System.gc(),會導致Stop-the-World的發生。

4. 垃圾回收的並行與併發

4.1 併發(Concurrent)

  • 在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理器上執行。
  • 併發不是真正意義上的“同時進行”,只是CPU把一個時間段劃分成幾個時間片段(時間區間),然後在這幾個時間區間之間來回切換,由於CPU處理的速度非常快,只要時間間隔處理得當,即可讓使用者感覺是多個應用程式同時在進行。

圖12-2 併發

4.2 並行(Parallel)

  • 當系統有一個以上CPU時,當一個CPU執行一個程式時,另一個CPU可以執行另一個程式,兩個程式互不搶佔CPU資源,可以同時進行,我們稱之為並行。
  • 其實決定並行的因素不是CPU的數量,而是CPU的核心數量,比如一個CPU多個核也可以並行。
  • 適合科學計算,後臺處理等弱互動場景。

圖12-3 並行

4.3 併發和並行對比

  • 併發:指的是多個事情,在同一時間段內同時發生了。
  • 並行:指的是多個事情,在同一時間點上同時發生了。
  • 併發的多個任務之間是互相搶佔資源的。
  • 並行的多個任務之間是不互相搶佔資源的。
  • 只有在多CPU或者一個CPU多核的情況中,才會發生並行。否則,看似同時發生的事情,其實都是併發執行的。

4.4 垃圾回收的併發與並行

併發和並行,在談論垃圾收集器的上下文語境中,它們可以解釋如下:

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態。比如:ParNew、Parallel Scavenge、Parallel Old。
  • 序列(Serial):相較於並行的概念,單執行緒執行。如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程式的執行緒。
  • 併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。使用者程式在繼續執行,而垃圾收集程式執行緒執行於另一個CPU上。比如:CMS、G1。

圖12-4 垃圾回收的並行與序列

圖12-5 垃圾回收的併發

5. 安全點與安全區域

  • 程式執行時並非在所有地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC,這些位置稱為“安全點(Safepoint)”。
  • 安全點的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致執行時的效能問題。大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程式長時間執行的特徵”來選取安全點。比如:選擇一些執行時間較長的指令作為安全點,如方法呼叫、迴圈跳轉和異常跳轉等。
  • 如何在GC發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢?
    • 搶先式中斷:(目前沒有虛擬機器採用了)首先中斷所有執行緒,如果還有執行緒不在安全點,就恢復執行緒,讓執行緒跑到安全點。
    • 主動式中斷:設定一箇中斷標誌,各個執行緒執行到安全點的時候主動輪詢這個標誌,如果中斷標誌為真,則將自己進行中斷掛起。
  • Safepoint機制保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程式“不執行”的時候呢?例如執行緒處於Sleep狀態或Blocked狀態,這時候執行緒無法響應JVM的中斷請求,“走”到安全點去中斷掛起,JVM也不太可能等待執行緒被喚醒。對於這種情況,就需要安全區域(Safe Region)來解決。
  • 安全區域是指在一段程式碼片段中,物件的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。我們也可以把Safe Region看做是被擴充套件了的Safepoint。
  • 實際執行時:
    • 當執行緒執行到Safe Region的程式碼時,首先標識已經進入了Safe Relgion,如果這段時間內發生GC,JVM會忽略標識為Safe Region狀態的執行緒。
    • 當執行緒即將離開Safe Region時,會檢查JVM是否已經完成GC,如果完成了,則繼續執行,否則執行緒必須等待直到收到可以安全離開Safe Region的訊號為止。

6. 再談引用

  • 我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是很緊張,則可以拋棄這些物件。

面試題

  • 強引用、軟引用、弱引用、虛引用有什麼區別?具體使用場景是什麼?
  • 在JDK1.2版之後,Java對引用的概念進行了擴充,將引用分為:
    • 強引用(Strong Reference)
    • 軟引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虛引用(Phantom Reference)
  • 這4種引用強度依次逐漸減弱。除強引用外,其他3種引用均可以在java.lang.ref包中找到它們的身影。如下圖,顯示了這3種引用型別對應的類,開發人員可以在應用程式中直接使用它們。

圖12-6 軟引用、弱引用、虛引用
  • Reference子類中只有終結器引用(FinalReference)是包內可見的,其他3種引用型別均為public,可以在應用程式中直接使用。
    • 強引用(StrongReference):最傳統的“引用”的定義,是指在程式程式碼之中普遍存在的引用賦值,即類似Object obj = new Object()這種引用關係。在任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。
    • 軟引用(SoftReference):在系統將要發生記憶體溢位之前,將會把這些物件列入回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
    • 弱引用(WeakReference):被弱引用關聯的物件只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱引用關聯的物件。
    • 虛引用(PhantomReference):一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個物件的例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
  • Java中的四種引用的作用:利於程式設計師通過程式碼的方式來決定某個物件的生命週期;利於垃圾回收。

6.1 強引用

  • 在Java程式中,最常見的引用型別是強引用(普通系統99%以上都是強引用),也就是我們最常見的普通物件引用,也是預設的引用型別。
  • 當在Java語言中使用new操作符建立一個新的物件,並將其賦值給一個變數的時候,這個變數就成為指向該物件的一個強引用。
  • 強引用的物件是可觸及的,垃圾收集器就永遠不會回收掉被引用的物件
  • 對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。
  • 軟引用、弱引用和虛引用的物件是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成Java記憶體洩漏的主要原因之一
package com.java.reference;

/**
 * @author rrqstart
 * @Description 強引用:不回收
 */
public class StrongReferenceTest {
    public static void main(String[] args) {
        StringBuffer str = new StringBuffer ("Hello!"); //str為強引用
        StringBuffer str1 = str; //str1也為強引用

        str = null;
        System.gc(); //此時仍不可回收str指向的物件,因為該物件還有str1引用指向

        try {
            Thread.sleep(3000); //3s延遲,保證手動GC執行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(str1); //Hello!
    }
}

本例中的兩個引用都是強引用,強引用具備以下特點:

  • 強引用可以直接訪問目標物件。
  • 強引用所指向的物件在任何時候都不會被系統回收,虛擬機器寧願丟擲OOM異常,也不會回收強引用所指向物件。
  • 強引用可能導致記憶體洩漏。

6.2 軟引用

  • 軟引用是用來描述一些還有用,但非必需的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收(第一次回收是針對不可觸及的物件),如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
  • 軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。
  • 垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地把引用存放到一個引用佇列(Reference Queue)。
  • 類似弱引用,只不過Java虛擬機器會盡量讓軟引用的存活時間長一些,迫不得已才清理。
  • 在JDK1.2版之後提供了java.lang.ref.SoftReference類來實現軟引用。

軟引用:

  • 當記憶體足夠時,不會回收軟引用可達的物件。
  • 當記憶體不夠時,會回收軟引用可達的物件。
package com.java.reference;

import java.lang.ref.SoftReference;

/**
 * @author rrqstart
 * @Description 軟引用:記憶體不足即回收
 * VM Args : -Xms10m -Xmx10m -XX:+PrintGCDetails
 */
public class SoftReferenceTest {
    public static class User {
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int id;
        public String name;

        @Override
        public String toString() {
            return "[id=" + id + ", name=" + name + "] ";
        }
    }

    public static void main(String[] args) {
        //建立物件,建立軟引用
        SoftReference<User> userSoftRef = new SoftReference<>(new User(1, "rrqstart"));

        /*
        下面的三行程式碼等價於上面的一行程式碼
        User user = new User(1,"rrqstart"); //宣告強引用
        SoftReference<User> userSoftRef = new SoftReference<>(user); //建立一個軟引用
        user = null; //銷燬該強引用之後堆記憶體中的User型別的物件才只有一個軟引用指向它
        */

        System.out.println(userSoftRef.get()); //get():獲取軟引用封裝的實體物件

        System.gc();
        System.out.println("After GC:");
        //垃圾回收之後獲取軟引用中的物件。
        System.out.println(userSoftRef.get()); //由於堆空間記憶體足夠,所有不會回收軟引用的可達物件。

        try {
            //讓系統認為記憶體資源緊張、不夠
            byte[] b = new byte[1024 * 1024 * 7]; //此時會回收軟引用可達物件,並報OOM
//            byte[] b = new byte[1024 * 7168 - 1024 * 590]; //此時會回收軟引用可達物件,但不報OOM
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            //再次從軟引用中獲取物件資料
            System.out.println(userSoftRef.get()); //在報OOM之前,垃圾回收器會回收軟引用的可達物件。
        }
    }
}

6.3 弱引用

  • 弱引用也是用來描述那些非必需物件,只被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的物件。
  • 但是,由於垃圾回收器的執行緒通常優先順序很低,因此,並不一定能很快地發現持有弱引用的物件。在這種情況下,弱引用物件可以存在較長的時間。
  • 弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用佇列,當弱引用物件被回收時,就會加入指定的引用佇列,通過這個佇列可以跟蹤物件的回收情況。
  • 軟引用、弱引用都非常適合來儲存那些可有可無的快取資料。如果這麼做,當系統記憶體不足時,這些快取資料會被回收,不會導致記憶體溢位。而當記憶體資源充足時,這些快取資料又可以存在相當長的時間,從而起到加速系統的作用。
  • 在JDK1.2版之後提供了java.lang.ref.WeakReference類來實現弱引用。
  • 弱引用物件與軟引用物件的最大不同就在於,當GC在進行回收時,需要通過演算法檢查是否回收軟引用物件,而對於弱引用物件,GC總是進行回收。弱引用物件更容易、更快被GC回收。

面試題:你開發中使用過WeakHashMap嗎?

  • 弱引用的應用:ThreadLocal、WeakHashMap。
  • WeakHashMap用來儲存圖片資訊,可以在記憶體不足的時候,及時回收,避免了OOM。
package com.java.reference;

import java.lang.ref.WeakReference;

/**
 * @author rrqstart
 * @Description 弱引用:發現即回收
 */
public class WeakReferenceTest {
    public static class User {
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int id;
        public String name;

        @Override
        public String toString() {
            return "[id=" + id + ", name=" + name + "] ";
        }
    }

    public static void main(String[] args) {
        //構造了弱引用
        WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "rrqstart"));

        /*
        下面的三行程式碼等價於上面的一行程式碼
        User user = new User(1,"rrqstart"); //宣告強引用
        WeakReference<User> userWeakRef = new WeakReference<>(user); //建立一個軟引用
        user = null; //銷燬強引用
         */

        //從弱引用中重新獲取物件
        System.out.println(userWeakRef.get()); //[id=1, name=rrqstart]

        System.gc();
        //不管當前記憶體空間足夠與否,都會回收它的記憶體
        System.out.println("After GC:");
        //重新嘗試從弱引用中獲取物件
        System.out.println(userWeakRef.get()); //null
    }
}

6.4 虛引用

  • 虛引用也稱為“幽靈引用”或者“幻影引用”,是所有引用型別中最弱的一個。
  • 一個物件是否有虛引用的存在,完全不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。
  • 虛引用不能單獨使用,也無法通過虛引用來獲取被引用的物件。當試圖通過虛引用的 get() 方法獲取物件時,總是null。
  • 為一個物件設定虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個物件被收集器回收時收到一個系統通知
  • 虛引用必須和引用佇列java.lang.ref.ReferenceQueue一起使用。虛引用在建立時必須提供一個引用佇列作為引數。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件後,將這個虛引用加入引用佇列,以通知應用程式物件的回收情況。
  • 由於虛引用可以跟蹤物件的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。
  • 在JDK1.2版之後提供了java.lang.ref.PhantomReference類來實現虛引用。
package com.java.reference;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

/**
 * @author rrqstart
 * @Description 虛引用:物件回收跟蹤
 */
public class PhantomReferenceTest {
    public static PhantomReferenceTest obj; //當前類物件的宣告
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; //引用佇列

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while (true) {
                if (phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> objt = null;
                    try {
                        objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (objt != null) {
                        System.out.println("追蹤垃圾回收過程:PhantomReferenceTest例項被GC了");
                    }
                }
            }
        }
    }

    //finalize()方法只能被呼叫一次!
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("呼叫當前類的finalize()方法");
        obj = this; //復活當前物件
    }

    public static void main(String[] args) {
        Thread t = new CheckRefQueue();
        t.setDaemon(true); //設定為守護執行緒:當程式中沒有非守護執行緒時,守護執行緒也就執行結束。
        t.start();

        phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
        obj = new PhantomReferenceTest();
        //構造了PhantomReferenceTest型別的物件的虛引用,並指定了引用佇列
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);

        try {
            //不可獲取虛引用中的物件
            System.out.println(phantomRef.get()); //null

            //將強引用去除
            obj = null; //此時該物件只有虛引用

            //第一次進行GC,由於物件可復活,GC無法回收該物件
            System.out.println("第 1 次 gc");
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj 是 null");
            } else {
                System.out.println("obj 可用");
            }

            System.out.println("第 2 次 gc");
            obj = null;
            System.gc(); //一旦將obj物件回收,就會將此虛引用存放到引用佇列中。
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj 是 null");
            } else {
                System.out.println("obj 可用");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//執行結果:
null
第 1 次 gc
呼叫當前類的finalize()方法
obj 可用
第 2 次 gc
追蹤垃圾回收過程:PhantomReferenceTest例項被GC了
obj 是 null

6.5 終結器引用

  • 終結器引用java.lang.ref.FinalReference主要用於實現物件的finalize()方法。
  • 無需手動編碼,其內部配合引用佇列使用。
  • 在GC時,終結器引用入隊。由Finalizer執行緒通過終結器引用找到被引用物件呼叫它的finalize()方法,第二次GC時才回收被引用的物件。

相關文章