詳解Java垃圾回收器

詳解Java垃圾回收器

上文講述了垃圾回收算法,本文介紹垃圾回收器,也就是垃圾回收算法的具體實現。

垃圾回收系統一般是基於分代收集策略,所以一個完整的垃圾回收系統一般是新生代垃圾收集器和老年代垃圾收集器搭配使用。唯一特別的是G1垃圾收集器,不僅可以對新生代垃圾進行回收,也可以對老年代垃圾進行回收。下圖是各個新生代收集器和老年代收集器搭配使用的情況,下文將詳細講述各個新生代垃圾收集器和老年代垃圾收集器。

img

新生代垃圾回收器

Serial

Serial收集器是最古老,最穩定以及效率高的收集器,只使用一個線程對垃圾進行收集,收集過程中,需要進行Stop The World,即需要暫停用戶線程,有可能造成長時間的停頓。詳細過程如下如圖。

ParNew

ParNew收集器本質是Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其餘行爲和Serial收集器完全一致。在Server模式下,ParNew收集器是一個非常重要的新生代收集器,因爲除Serial外,目前只有它能與CMS收集器配合工作;但在單個CPU環境中,不會比Serail收集器有更好的效果,因爲存在線程交互開銷。可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

Parallel Scavenge

Parallel Scavenge垃圾收集器因爲與吞吐量關係密切,也稱爲吞吐量收集器(Throughput Collector)。有一些特點與ParNew收集器相似:新生代收集器;採用複製算法;多線程收器集;CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間;Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput),即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;

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

老年代垃圾回收器

Serial Old

Serial的老年版本,使用標記整理算法,主要有兩個用處:1.早期與Parallel Scavenge搭配使用。2.作爲CMS的後備預案,在併發收集發生Concurrent Model Failure時使用。後面講述CMS的時候會提及。

Parallel Old

Parallel Scavenge的老年版本,同樣也是使用標記整理算法。主要用來與Parallel Scavenge搭配使用

img

CMS收集器

該收集器是一種以獲取最短回收停頓時間爲目標的收集器,使用“標記-清除”算法實現,整個過程分爲4個步驟

  • 初始標記(CMS initial mark):僅僅是標記一下GC Roots能直接關聯到的對象,速度很快。
  • 併發標記(CMS concurrent mark):進行GC Roots的Tracing的過程.
  • 重新標記(CMS remark):爲了修正在併發標記期間因用戶程序繼續運行而導致的標記產生變動的那一部分對象的標記記錄。
  • 併發清除(CMS concurrent): 清除垃圾的過程。

初始標記階段和重新標記階段需要暫停所有的用戶線程。

在CMS垃圾收集器工作時,需要藉助年輕代來判斷當前老年代中的對象是否是存活着的。如下圖所示,無法在老年代中直接使用GC ROOT TRACING來判斷老年代的對象的存活狀態。爲了找出並標記老年代存活的對象,需要掃描年輕代中的對象。由於年輕代中的對象較多,一般會採取先進行一次Minor GC使得年輕代的對象大幅度減少,也即會進行一次併發預清理階段

老年代的機制與一個叫CARD TABLE的東西密不可分。CMS將老年代的空間分成大小爲512bytes的塊,card table中的每個元素對應着一個塊。併發標記階段會把引用發生變化的老年對象所在的Card標識爲Dirty,後續重新標記階段就只需要掃描這些Dirty Card的對象,從而避免掃描整個老年代。

舉個例子:

  • 併發標記時對象的狀態:

  • 但隨後current obj的引用發生了變化:

    current obj所在的塊被標記爲dirty card.隨後到了重新標記階段,

    通過currrent obj變得可達的對象也被重新標記了,變成下面這樣

CMS收集器有以下3個明顯的缺點:

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

  • CMS收集器無法處理浮動垃圾(Floating Garbage),由於CMS在併發清理階段用戶線程還在運行着,伴隨着程序的運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留到下一次GC時再清理掉,這一部分垃圾就稱爲“浮動垃圾”。

    因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留有足夠的內存空間給用戶線程使用。若CMS運行期間預留的內存無法滿足用戶程序的需要,就會出現一次“Concurrent Mode Failure”失敗。這時虛擬機會臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間會更長。

  • 由於CMS採用的是標記清除算法,因此意味着收集結束時會有大量空間碎片產生,碎片產生,需要進行內存碎片整理,而碎片整理過程無法併發,因此會增加用戶線程的停頓時間。

G1收集器

將G1與CMS進行比較,G1是更好的解決方案。第一個區別是G1是壓縮型收集器。G1的壓縮功能,足以完全避免使用細粒度的空閒內存進行分配。這大大簡化了收集器,並且消除了大部分的潛在碎片問題。此外,G1比CMS收集器提供可預測的垃圾回收暫停時間

  • Remembered Set

    G1被分爲多個Region。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序對Reference類型數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之間,如果是便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。當內存回收時,在GC根節點的枚舉範圍加入Remembered Set即可保證不對全局堆掃描也不會有遺漏。

  • G1工作原理概述

    G1收集器採取不同的方法。堆被分成一組大小相等的區域,每個是連續範圍的虛擬內存。某些Regions被分配給和常規收集器一樣的角色(eden區,survivor區,老年代),但他們沒有固定的大小。這提供了更大的內存使用靈活性。

    G1 Heap Structure

    G1收集器與CMS收集器相比有兩個顯著的改進:一是G1收集器是基於標記整理算法實現的收集器,也就是說它不會產生空間碎片。二是它可以非常精確地控制停頓,既能讓使用者明確指定一個長度爲M毫秒的時間片段裏,消耗在垃圾收集上的時間不得超過N毫秒。G1將整個Java堆(包括新生代、老年代)劃分爲多個大小固定的獨立區域(Region),並且跟蹤這些區域裏面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是GarbageFirst名稱的來由)。區域劃分及有優先級的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。

    回收過程與CMS類似也是分爲四個階段

    • 初始標記(Initial Marking):僅僅標記GC Roots能直接關聯到的對象。
    • 併發標記(Concurrent Marking):從GC Roots開始對堆中對象進行可達性分析,找出存活的對象。
    • 最終標記(Final Marking):爲了修正在併發標記期間因用戶線程繼續運行而導致標記產生變動的把一部分記錄,虛擬機將這段時間對象變化記錄在線程Remember Set Logs裏面,最終標記階段需要把Remember Set Logs的數據合併到Remember Set中,這階段可以需要暫停用戶線程,也可以進行併發。
    • 篩選回收(Live Data Counting and Evacuation):該階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

參考文章

深入理解Java虛擬機

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章