Java垃圾回收—— 垃圾收集器

  • 概述
  • 垃圾收集器
  • 垃圾收集器組合
  • 概述

    在我們上一篇文章垃圾回收機制中講述到了垃圾回收的三個要點 :

    1. 哪些內存需要回收?
    2. 什麼時候回收?
    3. 如何回收?

    當然這些是內存回收的理論方法,那麼今天要介紹的就是內存回收的具體實現。

    但是瞭解垃圾收集器之前,我們還需要了解如下四個知識點。


    Stop The World

    可達性分析對執行時間的敏感還體現在GC停頓上。

    因爲這項分析工作必須在一個能確保一致性快照中進行——這裏“一致性”的意思是在指整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關係還在不斷變化的情況。

    這點是導致GC進行時必須停頓所有Java執行線程的重要原因之一。

    換個通俗易懂的說話就是 : 判定垃圾回收的時候要保持整個引用不改變,(我在打掃房間,數地板上的垃圾時,不允許別人清理或者增加垃圾,否則就亂套了)。

    所以當可達性分析引用鏈的時候,就要全部暫停(STOP!),但是這個暫停時間特別短暫,對程序的影響也是微乎其微的,這就是GC卡頓的原因由來。


    枚舉GC Roots

    從可達性分析中從GC Roots節點找引用鏈這個操作爲例,可作爲GC Roots的節點主要在全局性的引用(例如常量或者靜態屬性)與執行上下文(棧幀中的局部變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裏面的引用,那麼必然會消耗很多時間。

    上面提到 : 可達性分析對執行時間的敏感還體現在GC停頓上,也就是說枚舉這些根節點的效率將會影響到GC的停頓時間。當然在虛擬機中還是有辦法直接得知哪些地方存放着對象的引用。

    在HotSpot的實現中,使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成之後,HotSpot就會把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也只會在特定的位置(安全點)在記錄下棧和寄存器中哪些位置是引用。


    安全點

    在OopMap的協助下,HotSpot可以快速且準確的完成GC Roots枚舉,只會在特定的位置記錄了這些信息,這些位置稱爲安全點(Safepoint)。

    只有在安全點程序才能暫停下來開始GC,並且安全點的選定標準是 : 是否具有讓程序長時間執行的特徵爲標準 。也就是說這個安全點不能因爲指令長時間執行而導致停頓時間長。一般來說下面這三種情況的指令纔會產生SafePoint。

    • 方法調用(臨返回前/調用方法的call指令後)
    • 循環跳轉(循環的末尾)
    • 異常跳轉(可能拋出異常的位置)

    另外一個問題是 : 如何在GC發生時讓所有的線程都“跑”到最近的安全點上再停頓下來?

    在JVM中採用的是主動式中斷思想。

    主動式中斷 : 當GC需要中斷線程時,不直接對線程操作,簡單的在安全點設置一個標誌,各個線程執行到安全點的時候主動去輪詢這個標誌,發現中斷標誌爲true就自己中斷掛起。


    總結一下,安全點的主要作用是:

    • OopMap記錄GC Roots信息。
    • 設置GC標誌,當需要GC的時候,中斷掛起所有執行的線程。


    安全區域

    但是如果線程處於Sleep或者Blocked狀態時,無法響應JVM的中斷請求。

    也就是說無法主動“跑”到安全點的位置並中斷掛起。那麼就需要安全區域(Safe Region)來處理。

    安全區域是指在一段代碼之中,引用關係不會發生變化。在這個區域的任意地方開始GC都是安全的。

    所以只要線程執行到了Safe Region中的代碼時做到以下兩點即可 :

    1. 首先標識自己已經進入了Safe Region。
    2. 在線程要離開Safe Region時,檢查系統是否完成了GC過程,如果完成了,那就繼續執行。否則就等待到GC執行完畢的信號爲止。

    ————————– 事不關己高高掛起,等你們處理完了我纔來瞎摻和。 ————————–



    垃圾收集器

    瞭解垃圾收集器之前,我們先了解在垃圾回收器中以下幾個名詞

    吞吐量

    CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。

    吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間 + 垃圾收集時間)。

    比如說虛擬機總運行了100分鐘,用戶代碼時間99分鐘,垃圾收集時間1分鐘,那麼吞吐量就是99%。


    GC的名詞

    • 新生代GC(Minor GC) : 指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

    • 老年代GC(Major GC) : 指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(發生這種情況,那麼整個堆都GC一遍,通常稱爲Full GC)。Major GC的速度一般會比Minor GC慢10倍以上。


    並行和併發

    • 串行 (Parallel) : 單線程垃圾收集工作,但此時用戶線程仍然處於等待狀態。

    • 並行 (Parallel) : 指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。(與串行相比就是回收的執行過程中,多了幾條垃圾收集線程並行工作)

    • 併發 (Concurrent) : 指用戶線程與垃圾收集線程同時執行(但並不一定是並行的,可能會交替執行),用戶程序在繼續執行,而垃圾收集程序運行於另一個CPU上。


    這裏寫圖片描述


    這裏寫圖片描述





    瞭解完這些名詞之後,我們開始瞭解7種不同的垃圾收集器。

    垃圾收集器是內存回收的具體實現,並且虛擬機會根據不同場景使用不同的垃圾回收器組合。

    常見的7種垃圾收集器有:

    • serial收集器、ParNew收集器(seria收集器的升級版,新生代)、serial Old收集器。

    • Parallel Scavenge收集器、Parallel Old 收集器。(吞吐量優先)

    • CMS(Concurrent Mark-Sweep)收集器。老年代。(停頓時間優先)

    • G1收集器(Garbage First)。整個堆。

    其中Serial收集器爲串行收集器,其他均爲並行收集器。


    Serial 收集器(單線程垃圾收集)(新,老)

    ——————– 最古老,最穩定,簡單而高效,可能會產生較長的停頓。 ——————–

    Serial是一個單線程的收集器,它不僅僅只會使用一個CPU或一條線程去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結束。

    Serial垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對於限定單個CPU環境來說,沒有線程交互的開銷,可以獲得最高的單線程垃圾收集效率,因此Serial垃圾收集器依然是java虛擬機運行在Client模式下默認的新生代垃圾收集器。


    這裏寫圖片描述

    如上圖所示,Serial 收集器在新生代和老年代都有對應的版本,除了收集算法不同,兩個版本並沒有其他差異。

    • Serial 新生代收集器採用的是複製算法。
    • Serial Old 老年代採用的是標記 - 整理算法。


    ParNew 收集器(Serial 升級版)(新)

    ——————– Serial 新生代收集器升級版,多線程垃圾收集 ——————–

    ParNew收集器其實就是Serial新生代收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣。

    在實現上,這兩種收集器也共用了相當多的代碼。ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。


    這裏寫圖片描述



    Parallel 收集器(吞吐量優先)(新,老)

    ——————– 與ParNew收集器基本一樣,但是更關注吞吐量 ——————–

    Parallel 收集器相對於其它收集器最大的特點和關注點就是達到一個可控制的吞吐量。

    上面也介紹過吞吐量這個名詞,也就是垃圾收集收集對於總時間的佔比。

    停頓時間與吞吐量 :

    停頓時間越短越適合需要用戶交互的程序,良好的響應速度能提升用戶體驗

    吞吐量小則可以高效率的利用CPU時間,儘快完成程序的運算任務,主要適合後臺運算而且不需要太多交互的任務。

    當然GC的停頓時間和吞吐量以及內存空間是有很大關聯的 : GC更新頻繁(吞吐量佔比高),內存空間分配小(收集速度更快)。


    這裏寫圖片描述

    如上圖所示,Parallel 收集器在新生代和老年代也都有對應的版本,除了收集算法不同,兩個版本並沒有其他差異。
    並且Parallel 兩個年代的收集器也可以說是 Serial 收集器的升級版,單線程(串行收集) – > 多線程(並行收集)。

    • Parallel Scavenge 新生代收集器採用的是複製算法。
    • Parallel Old 老年代採用的是標記 - 整理算法。

    在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器組合。



    CMS 收集器(停頓時間優先)(老)

    ——————– 最短回收停頓時間爲目標的收集器,提供最好的用戶體驗 ,ART老年代默認收集器 ——————–

    CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。

    CMS收集器是基於“標記 - 清除”算法實現的。也就是說在JVM中它是一款老年代垃圾收集器


    這裏寫圖片描述

    CMS收集器運作過程相對比上面幾種收集器來說更復雜一些,整個過程分爲4個步驟 :

    • 初始標記(CMS initial mark)
      初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。(OopMap)

    • 併發標記(CMS concurrent mark)
      併發標記階段就是進行GC Roots Tracing的過程。(從GC Roots 開始對堆進行可達性分析,找出存活對象。)

    • 重新標記(CMS remark)
      重新標記階段是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要“Stop The World”。

    • 併發清除(CMS concurrent sweep)
      併發清除階段會清除對象。

    CMS是一款優秀的收集器,它的主要優點是 : 併發手機、低停頓。並且它還是ART老年代默認收集器

    但是CMS還達不到完美的程度,它有以下3個明顯缺點 :

    1. CMS收集器對CPU資源非常敏感,因爲在併發階段它雖不會導致用戶線程停頓,但是佔用一部分線程(CPU資源),導致應用程序變慢,總吞吐量變低。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。

    2. CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
      也是由於CMS垃圾併發收集階段用戶線程還需要運行,伴隨着程序運行自然還會有新的垃圾不斷產生,這一部分的垃圾出現在標記過程之後,CMS無法在當次收集中處理它們,只好留待下一次GC時再清理。(所以也需要預存空間存放這些浮動垃圾。)

    3. CMS是一款基於“標記—清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。不過CMS頂不住要FullGC時會開啓內存碎片的合併整理過程。內存整理的過程是無法併發的,所以停頓的時間又會邊長。



    G1收集器(垃圾區域Region優先)(整個堆)

    ——————– 可預測的停頓時間,整個堆劃分成多個區域 ——————–

    G1(Garbage - First)是一款面向服務端應用的垃圾收集器。基於“標記 - 整理”算法。

    它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離了,它們都是一部分Region(不需要連續)的集合。

    G1(Garbage - First)名稱的由來是G1跟蹤各個Region裏面的垃圾堆的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region

    這種使用Region劃分內存空間以及有優先級的區域回收方式,保證G1收集器在有限的時間內可以獲得儘可能高的收集效率。

    與其它GC收集器相比,G1具備如下4個特點 :

    • 並行與併發
      使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行。

    • 分代收集
      與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。新生代和老年代不再是物理隔離了,是多個大小相等的獨立Region。

    • 空間整合
      與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC。

    • 可預測的停頓
      這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。(後臺維護的優先列表,優先回收價值大的Region)

    並且在G1收集器中,每個Region都有一個與之對應Remember Set來避免全堆掃描。並且還可以根據這個Remember Set來檢查Reference 引用的對象是否處於不同的Region之中。


    這裏寫圖片描述

    G1收集器和CMS收集器運作過程有很多相似之處,整個過程也分爲4個步驟 :

    • 初始標記
      初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。(OopMap)

    • 併發標記
      併發標記階段就是進行GC Roots Tracing的過程。(從GC Roots 開始對堆進行可達性分析,找出存活對象。)

    • 最終標記
      最終標記和CMS的重新標記階段一樣,也是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要“Stop The World”。(修正Remebered Set)

    • 篩選回收
      首先對各個Region的回收價值和成本進行排行,根據用戶所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與用戶程序一起併發執行,但是因爲只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。

    如果你的應用追求低停頓,那G1現在已經可以作爲一個可嘗試選擇,如果你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。



    垃圾收集器組合

    Java虛擬機規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別,並且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。


    這裏寫圖片描述

    圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。

    虛擬機所處的區域,則表示它是屬於新生代收集器還是老年代收集器。

    轉載:https://blog.csdn.net/qian520ao/article/details/79050982
    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章