首頁 > 軟體

Java垃圾回收機制的範例詳解

2022-04-19 13:01:28

一、概述

說起垃圾收集(Garbage Collection,下文簡稱GC),有不少人把這項技術當作Java語言的伴生產 物。事實上,垃圾收集的歷史遠遠比Java久遠,在1960年誕生於麻省理工學院的Lisp是第一門開始使 用記憶體動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,其作者John McCarthy就思考過垃圾 收集需要完成的三件事情:

  • 哪些記憶體需要回收?
  • 什麼時候回收?
  • 如何回收?

經過半個世紀的發展,今天的記憶體動態分配與記憶體回收技術已經相當成熟,一切看起來都進入 了“自動化”時代,那為什麼我們還要去了解垃圾收集和記憶體分配?

答案很簡單:當需要排查各種記憶體 溢位、記憶體漏失問題時,當垃圾收整合為系統達到更高並行量的瓶頸時,我們就必須對這些“自動 化”的技術實施必要的監控和調節。

Java記憶體運 行時區域其中程式計數器、虛擬機器器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅,棧 中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基 本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性, 在這幾個區域內就不需要過多考慮如何回收的問題,當方法結束或者執行緒結束時,記憶體自然就跟隨著 回收了。

而Java堆和方法區這兩個區域則有著很顯著的不確定性:一個介面的多個實現類需要的記憶體可能 會不一樣,一個方法所執行的不同條件分支所需要的記憶體也可能不一樣,只有處於執行期間,我們才 能知道程式究竟會建立哪些物件,建立多少個物件,這部分記憶體的分配和回收是動態的。垃圾收集器 所關注的正是這部分記憶體該如何管理,本文後續討論中的“記憶體”分配與回收也僅僅特指這一部分記憶體。

二、物件已死?

在堆裡面存放著Java世界中幾乎所有的物件範例,垃圾收集器在對堆進行回收前,第一件事情就 是要確定這些物件之中哪些還“存活”著,哪些已經“死去”(“死去”即不可能再被任何途徑使用的對 象)了。

這個面試的時候也經常會問到,依靠兩個演演算法來判斷物件是否存活:

1、參照計數演演算法

2、可達分析演演算法。

1.參照計數演演算法

判斷物件是否存活的演演算法是這樣的:在物件中新增一個參照計數器,每當有一個地方 參照它時,計數器值就加一;當參照失效時,計數器值就減一;任何時刻計數器為零的物件就是不可 能再被使用的。

客觀地說,參照計數演演算法(Reference Counting)雖然佔用了一些額外的記憶體空間來進行計數,但 它的原理簡單,判定效率也很高,很多領域都在用,但是,在Java 領域,至少主流的Java虛擬機器器裡面都沒有選用參照計數演演算法來管理記憶體。原因:單純的參照計數 就很難解決物件之間相互迴圈參照的問題。

程式碼範例:

在下面testGC()方法:物件objA和objB都有欄位instance,賦值令 objA.instance=objB及objB.instance=objA,除此之外,這兩個物件再無任何參照,實際上這兩個物件已 經不可能再被存取,但是它們因為互相參照著對方,導致它們的參照計數都不為零,參照計數演演算法也 就無法回收它們。

對於objA = null,objB = null應該有的人會不理解為什麼要這麼做,咱們要測試的是屬性出現了互相參照,是否會被gc掉。那麼我就得保證其他地方不再參照他,我們用的是main方法測試,所以需要設定為null。正常在java應用是不需要的,執行緒執行過後,區域性變數將被銷燬。那也就不存在參照這一說了。

雖然手動的讓區域性變數不在參照堆中的物件,但是在堆記憶體當中這兩個物件的屬性還是相互參照的。如果按照計數演演算法,他是不應該被gc掉的。

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /*** 這個成員屬性的唯一意義就是佔點記憶體,以便能在GC紀錄檔中看清楚是否有回收過 */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假設在這行發生GC,objA和objB是否能被回收?
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

設定啟動引數:輸出GC的詳細紀錄檔

-XX:+PrintGCDetails

執行結果:

從執行結果中可以清楚看到記憶體回收紀錄檔中包含“9257K->823K”,意味著虛擬機器器並沒有因為這兩 個物件互相參照就放棄回收它們,這也從側面說明了Java虛擬機器器並不是通過參照計數演演算法來判斷物件 是否存活的。

2.可達性分析演演算法

當前主流的商用程式語言(Java、C#,上溯至前面提到的古老的Lisp)的記憶體管理子系統,都是 通過可達性分析(Reachability Analysis)演演算法來判定物件是否存活的。這個演演算法的基本思路就是通過 一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據參照關係向下搜尋,搜尋過 程所走過的路徑稱為“參照鏈”(Reference Chain),如果某個物件到GC Roots間沒有任何參照鏈相連, 或者用圖論的話來說就是從GC Roots到這個物件不可達時,則證明此物件是不可能再被使用的。

如圖所示,物件object 5、object 6、object 7雖然互有關聯,但是它們到GC Roots是不可達的, 因此它們將會被判定為可回收的物件。

在Java技術體系裡面,固定可作為GC Roots的物件包括以下幾種:

  1. 在虛擬機器器棧(棧幀中的本地變數表)中參照的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的 引數、區域性變數、臨時變數等。
  2. 在方法區中類靜態屬性參照的物件,譬如Java類的參照型別靜態變數。
  3. 在方法區中常數參照的物件,譬如字串常數池(String Table)裡的參照。
  4. 在本地方法棧中JNI(即通常所說的Native方法)參照的物件。
  5. Java虛擬機器器內部的參照,如基本資料型別對應的Class物件,一些常駐的異常物件(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類載入器。
  6. 所有被同步鎖(synchronized關鍵字)持有的物件。
  7. 反映Java虛擬機器器內部情況的JMXBean、JVMTI中註冊的回撥、原生程式碼快取等。
  8. 除了這些固定的GC Roots集合以外,根據使用者所選用的垃圾收集器以及當前回收的記憶體區域不 同,還可以有其他物件“臨時性”地加入,共同構成完整GC Roots集合。譬如後文將會提到的分代收集 和區域性回收(Partial GC),如果只針對Java堆中某一塊區域發起垃圾收集時(如最典型的只針對新生 代的垃圾收集),必須考慮到記憶體區域是虛擬機器器自己的實現細節(在使用者視角里任何記憶體區域都是不 可見的),更不是孤立封閉的,所以某個區域裡的物件完全有可能被位於堆中其他區域的物件所引 用,這時候就需要將這些關聯區域的物件也一併加入GC Roots集合中去,才能保證可達性分析的正確 性。

目前最新的幾款垃圾收集器無一例外都具備了區域性回收的特徵。如OpenJDK中的G1、Shenandoah、ZGC以及Azul的PGC、C4這些收集器。

3.四種參照

無論是通過參照計數演演算法判斷物件的參照數量,還是通過可達性分析演演算法判斷物件是否參照鏈可 達,判定物件是否存活都和“參照”離不開關係。在JDK 1.2版之前,Java裡面的參照是很傳統的定義: 如果reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱該reference資料是代表 某塊記憶體、某個物件的參照。一個物件在 這種定義下只有“被參照”或者“未被參照”兩種狀態,對於描述一些“食之無味,棄之可惜”的物件就顯 得無能為力。譬如我們希望能描述一類物件:當記憶體空間還足夠時,能保留在記憶體之中,如果記憶體空 間在進行垃圾收集後仍然非常緊張,那就可以拋棄這些物件——很多系統的快取功能都符合這樣的應 用場景。

在JDK 1.2版之後,Java對參照的概念進行了擴充,將參照分為強參照(Strongly Re-ference)、軟 參照(Soft Reference)、弱參照(Weak Reference)和虛參照(Phantom Reference)4種,這4種參照強 度依次逐漸減弱。

  • 強參照是最傳統的“參照”的定義,是指在程式程式碼之中普遍存在的參照賦值,即類似“Object obj=new Object()”這種參照關係。無論任何情況下,只要強參照關係還存在,垃圾收集器就永遠不會回 收掉被參照的物件。
  • 軟參照是用來描述一些還有用,但非必須的物件。只被軟參照關聯著的物件,在系統將要發生內 存溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體, 才會丟擲記憶體溢位異常。在JDK 1.2版之後提供了SoftReference類來實現軟參照。
  • 弱參照也是用來描述那些非必須物件,但是它的強度比軟參照更弱一些,被弱參照關聯的物件只 能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只 被弱參照關聯的物件。在JDK 1.2版之後提供了WeakReference類來實現弱參照。
  • 虛參照也稱為“幽靈參照”或者“幻影參照”,它是最弱的一種參照關係。一個物件是否有虛參照的 存在,完全不會對其生存時間構成影響,也無法通過虛參照來取得一個物件範例。為一個物件設定虛 參照關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2版之後提供 了PhantomReference類來實現虛參照。

4.生存還是死亡?

即使在可達性分析演演算法中判定為不可達的物件,也不是“非死不可”的,這時候它們暫時還處於“緩 刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒 有與GC Roots相連線的參照鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此物件是 否有必要執行finalize()方法。假如物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器器呼叫 過,那麼虛擬機器器將這兩種情況都視為“沒有必要執行”。

finalize()方法是Object的一個方法,所有的物件都是Object的子類。只不過語法上預設沒有繼承Object。但是實際上是繼承的Object類。

如果這個物件被判定為確有必要執行finalize()方法,那麼該物件將會被放置在一個名為F-Queue的 佇列之中,並在稍後由一條由虛擬機器器自動建立的、低排程優先順序的Finalizer執行緒去執行它們的finalize() 方法。這裡所說的“執行”是指虛擬機器器會觸發這個方法開始執行,但並不承諾一定會等待它執行結束。

這樣做的原因是,如果某個物件的finalize()方法執行緩慢,或者更極端地發生了死迴圈,將很可能導 致F-Queue佇列中的其他物件永久處於等待。

如果對 象要在finalize()中成功拯救自己 只要重新與參照鏈上的任何一個物件建立關聯即可,譬如把自己 (this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移出“即將回收”的集 合;如果物件這時候還沒有逃脫,那基本上它就真的要被回收了。從下面程式碼我們可以看到一個 物件的finalize()被執行,但是它仍然可以存活。

一次物件自我拯救的演示:

此程式碼演示了兩點:

1.物件可以在被GC時自我拯救。

2.這種自救的機會只有一次,因為一個物件的finalize()方法最多隻會被系統自動呼叫一次

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //物件第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因為Finalizer方法優先順序很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        // 下面這段程式碼與上面的完全相同,但是這次自救卻失敗了
        SAVE_HOOK = null;
        System.gc();
        // 因為Finalizer方法優先順序很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

執行結果:

finalize()方法大家儘量避免使用它,它的執行代價高昂,不確定性大,無法保證各個物件的呼叫順序,如今已被官方明確宣告為 不推薦使用的語法。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、 更及時。

5.回收方法區

方法區的垃圾收集主要回收兩部分內容:廢棄的常數和不再使用的型別。回收廢棄常數與回收 Java堆中的物件非常類似。舉個常數池中字面量回收的例子,假如一個字串“java”曾經進入常數池 中,但是當前系統又沒有任何一個字串物件的值是“java”,換句話說,已經沒有任何字串物件參照 常數池中的“java”常數,且虛擬機器器中也沒有其他地方參照這個字面量。如果在這時發生記憶體回收,這個“java”常數就將會被系統清理出常數池。常數池中其他類(接 口)、方法、欄位的符號參照也與此類似。

判定一個常數是否“廢棄”還是相對簡單,而要判定一個型別是否屬於“不再被使用的類”的條件就 比較苛刻了。需要同時滿足下面三個條件:

  • 該類所有的範例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的範例。
  • 載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如 OSGi、JSP的過載入等,否則通常是很難達成的。
  • 該類對應的java.lang.Class物件沒有在任何地方被參照,無法在任何地方通過反射存取該類的方 法。
  • Java虛擬機器器被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而並不是 和物件一樣,沒有參照了就必然會回收。關於是否要對型別進行回收,HotSpot虛擬機器器提供了命令引數進行控制,

-Xnoclassgc :關閉虛擬機器器對class的垃圾回收功能。
-verbose:class XXX :(XXX為程式名)你會在控制檯看到載入的類的情況。
-XX:+TraceClassLoading :監控類的載入
-XX:+TraceClassUnLoading : 監控類的解除安裝

其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虛擬機器器中使用,-XX:+TraceClassUnLoading引數需要FastDebug版的虛擬機器器支援。

在大量使用反射、動態代理、CGLib等位元組碼框架,動態生成JSP以及OSGi這類頻繁自定義類載入 器的場景中,通常都需要Java虛擬機器器具備型別解除安裝的能力,以保證不會對方法區造成過大的記憶體壓 力。

三、垃圾收集演演算法

Java預設的虛擬機器器HotSpot VM,採用的追蹤式垃圾收集,也就是剛剛所說的可達分析,所以本節介紹的所有演演算法均屬於追蹤式垃圾收集的範疇。

1.分代收集理論

當前商業虛擬機器器的垃圾收集器,大多數都遵循了“分代收集”的理論進 行設計,分代收集名為理論,實質是一套符合大多數程式執行實際情況的經驗法則,它建立在兩個分代假說之上:

1)弱分代假說(Weak Generational Hypothesis):絕大多數物件都是朝生夕滅的。

2)強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的物件就越難以消 亡

這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分 出不同的區域,然後將回收物件依據其年齡(年齡即物件熬過垃圾收集過程的次數)分配到不同的區 域之中儲存。

在Java堆劃分出不同的區域之後,垃圾收集器才可以每次只回收其中某一個或者某些部分的區域 ——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收型別的劃分;發展出了“標記-複製演演算法”“標記-清除算 法”“標記-整理演演算法”等針對性的垃圾收集演演算法,這一切的出現都始於分代收集理論。

把分代收集理論具體放到現在的商用Java虛擬機器器裡,設計者一般至少會把Java堆劃分為新生代 (Young Generation)和老年代(Old Generation)兩個區域。顧名思義,在新生代中,每次垃圾收集 時都發現有大批物件死去,而每次回收後存活的少量物件,將會逐步晉升到老年代中存放。分代收集並非只是簡單劃分一下記憶體區域那麼容易,它至少存在一個明顯的困難:物件不 是孤立的,物件之間會存在跨代參照。

假如要現在進行一次只侷限於新生代區域內的收集(Minor GC),但新生代中的物件是完全有可 能被老年代所參照的,為了找出該區域中的存活物件,不得不在固定的GC Roots之外,再額外遍歷整 個老年代中所有物件來確保可達性分析結果的正確性,反過來也是一樣。遍歷整個老年代所有物件 的方案雖然理論上可行,但無疑會為記憶體回收帶來很大的效能負擔。為了解決這個問題,就需要對分 代收集理論新增第三條經驗法則:

3)跨代參照假說(Intergenerational Reference Hypothesis):跨代參照相對於同代參照來說僅佔極 少數。

依據這條假說,我們就不應再為了少量的跨代參照去掃描整個老年代,也不必浪費空間專門記錄 每一個物件是否存在及存在哪些跨代參照,只需在新生代上建立一個全域性的資料結構(該結構被稱 為“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊記憶體會 存在跨代參照。此後當發生Minor GC時,只有包含了跨代參照的小塊記憶體裡的物件才會被加入到GC Roots進行掃描。雖然這種方法需要在物件改變參照關係(如將自己或者某個屬性賦值)時維護記錄數 據的正確性,會增加一些執行時的開銷,但比起收集時掃描整個老年代來說仍然是划算的。

2.名詞解釋

部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:

  • 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單 獨收集老年代的行為。另外請注意“Major GC”這個說法現在有點混淆,在不同資料上常有不同所指,有的是指老年代的收集、有的是指整堆收集。
  • 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收 集器會有這種行為。
  • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。

通常能單獨發生收集行為的只是新生代,所以這裡“反過來”的情況只是理論上允許,實際上除了 CMS收集器,其他都不存在只針對老年代的收集。

3.標記-清除演演算法

演演算法分為“標記”和“清除”兩個階段:首先標記出需要回 收的物件,在標記完成後,統一回收掉所有被標記的物件,也可以反過來,標記存活的物件,統一回 收所有未被標記的物件。標記過程就是物件是否屬於垃圾的判定過程,怎麼判斷是否垃圾,就是剛剛所提到的,參照計數演演算法和可達分析演演算法。

它的主要缺點有兩個:第一個是執行效率不穩定,如果Java堆中包含大量對 象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過 程的執行效率都隨物件數量增長而降低;第二個是記憶體空間的碎片化問題,標記、清除之後會產生大 量不連續的記憶體碎片,空間碎片太多可能會導致當以後在程式執行過程中需要分配較大物件時無法找 到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。標記-清除演演算法的執行過程如圖所示。

4.標記-複製演演算法

標記-複製演演算法常被簡稱為複製演演算法。為了 解決標記-清除演演算法面對大量可回收物件時執行效率低 的問題,1969年Fenichel提出了一種稱為“半區複製”(Semispace Copying)的垃圾收集演演算法,它將可用 記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著 的物件複製到另外一塊上面,複製的時候不用考慮有 空間碎片的複雜情況,只要移動堆頂指標,按順序分配即可,然後再把已使用過的記憶體空間一次清理掉。

缺點:

如果記憶體中多數物件都是存 活的,這種演演算法將會產生大量的記憶體間複製的開銷。

回收演演算法的代價是將可用記憶體縮小為了原來的一半,空間浪費未免太多了一 點。

在1989年,Andrew Appel針對具備“朝生夕滅”特點的物件,提出了一種更優化的半區複製分代策 略,現在稱為“Appel式回收”。HotSpot虛擬機器器的Serial、ParNew等新生代收集器均採用了這種策略來設 計新生代的記憶體佈局。

Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配記憶體只使用Eden和其中一塊Survivor。發生垃圾蒐集時,將Eden和Survivor中仍 然存活的物件一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機器器預設Eden和Survivor的大小比例是8∶1,也即每次新生代中可用記憶體空間為整個新 生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會 被“浪費”的。當然,98%的物件可被回收僅僅是“普通場景”下測得的資料,任何人都沒有辦法百分百 保證每次回收都只有不多於10%的物件存活,因此Appel式回收還有一個充當罕見情況的“逃生門”的安 全設計,當Survivor空間不足以容納一次Minor GC之後存活的物件時,就需要依賴其他記憶體區域(實 際上大多就是老年代)進行分配擔保。

記憶體的分配擔保好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,於是 銀行可能會預設我們下一次也能按時按量地償還貸款,只需要有一個擔保人能保證如果我不能還款 時,可以從他的賬戶扣錢,那銀行就認為沒有什麼風險了。記憶體的分配擔保也一樣,如果另外一塊 Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件,這些物件便將通過分配擔保機制直 接進入老年代,這對虛擬機器器來說就是安全的。

5.標記-整理演演算法

針對老年代物件的存亡特徵,1974年Edward Lueders提出了另外一種有針對性的“標記-整 理”(Mark-Compact)演演算法,其中的標記過程仍然與“標記-清除”演演算法一樣,但後續步驟不是直接對可 回收物件進行清理,而是讓所有存活的物件都向記憶體空間一端移動,然後直接清理掉邊界以外的內 存,“標記-整理”演演算法的示意圖如圖所示。

標記-清除演演算法與標記-整理演演算法的本質差異在於前者是一種非移動式的回收演演算法,而後者是移動 式的。是否移動回收後的存活物件是一項優缺點並存的風險決策:

移動則記憶體回收時會更復雜,不移動則記憶體分配時會 更復雜。移動雖然複雜點,但是不影響伺服器的記憶體吞吐量。HotSpot虛擬機器器裡面關注吞吐量的Parallel Scavenge收集器是基於標記-整理演演算法的。

還有一種“和稀泥式”解決方案可以不在記憶體分配和存取上增加太大額外負擔,做法是讓虛 擬機平時多數時間都採用標記-清除演演算法,暫時容忍記憶體碎片的存在,直到記憶體空間的碎片化程度已經 大到影響物件分配時,再採用標記-整理演演算法收集一次,以獲得規整的記憶體空間。前面提到的基於標 記-清除演演算法的CMS收集器面臨空間碎片過多時採用的就是這種處理辦法(CMS是老年代垃圾收集器)。

以上就是Java垃圾回收機制的範例詳解的詳細內容,更多關於Java垃圾回收機制的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com