深入理解Java虛擬機(六):內存回收實踐者——經典垃圾收集器

引言

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

收集器概覽

在這裏插入圖片描述
上圖展示了七種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。

收集器劃分

收集器可劃分爲並行和併發兩種。

  • 並行(Parallel):並行描述的是多條垃圾收集器線程之間的關係,說明同一時間有多條這樣的線程在協同工作,通常默認此時用戶線程是處於等待狀態。

  • 併發(Concurrent):併發描述的是垃圾收集器線程與用戶線程之間的關係,說明同一時間垃圾收集器線程與用戶線程都在運行。由於用戶線程並未被凍結,所以程序仍然能響應服務請求,但由於垃圾收集器線程佔用了一部分系統資源,此時應用程序的處理的吞吐量將受到一定影響。

收集器分述

Serial收集器

1)簡要介紹

Serial收集器是最基礎、歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是HotSpot虛擬機新生代收集器的唯一選擇。

2)收集模式

這個收集器是一個單線程工作的收集器,在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。

3)運行示意圖
Serial/Serial Old收集器
上圖爲Serial(新生代)/Serial Old(老年代)收集器運行圖。

4)優缺點

簡單而高效(與其他收集器的單線程相比),對於內存資源受限的環境,它是所有收集器裏額外內存消耗(Memory Footprint)最小的;

對於單核處理器或處理器核心數較少的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

“Stop The World”的用戶體驗較差。但垃圾收集的停頓時間完全可以控制在十幾、幾十毫秒,最多一百多毫秒以內,只要不是頻繁發生收集,這點停頓時間對許多用戶來說是完全可以接受的。

所以,Serial收集器對於運行在客戶端模式下的虛擬機來說是一個很好的選擇。

5)使用

HotSpot虛擬機運行在客戶端模式下的默認新生代收集器。

ParNew收集器

1)簡要介紹

ParNew收集器實質上是Serial收集器的多線程並行版本。

除了同時使用多條線程進行垃圾收集之外,其餘的行爲包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop TheWorld、對象分配規則、回收策略等都與Serial收集器完全一致。

2)運行示意圖

在這裏插入圖片描述
上圖爲ParNew(新生代)/Serial Old(老年代)收集器運行圖。

3)優缺點

ParNew收集器在單核心處理器的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程(Hyper-Threading)技術實現的僞雙核處理器環境中都不能百分之百保證超越Serial收集器。

隨着可以被使用的處理器核心數量的增加,ParNew對於垃圾收集時系統資源的高效利用還是很有好處的。它默認開啓的收集線程數與處理器核心數量相同,在處理器核心非常多(譬如32個,現在CPU都是多核加超線程設計,服務器達到或超過32個邏輯核心的情況非常普遍)的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

4) 使用

ParNew收集器是不少運行在服務端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統中首選的新生代收集器,其中有一個與功能、性能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器(下文會有詳細介紹)配合工作。

隨着垃圾收集器技術的不斷改進,更先進的G1收集器帶着CMS繼承者和替代者的光環登場。G1是一個面向全堆的收集器,不再需要其他新生代收集器的配合工作。

所以,自JDK 9開始,ParNew加CMS收集器的組合就不再是官方推薦的服務端模式下的收集器解決方案了。官方希望它能完全被G1所取代。

Parallel Scavenge收集器

1)簡要介紹

Parallel Scavenge收集器也是一款新生代收集器,它同樣是基於標記-複製算法實現的收集器,也是能夠並行收集的多線程收集器。

2)收集模式

Parallel Scavenge收集器與其他收集器關注點不同。

CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。

所以也被稱爲“吞吐量優先收集器”。

吞吐量,是指處理器用於運行用戶代碼的時間與處理器總消耗時間的比值,即:
在這裏插入圖片描述
如果虛擬機完成某個任務,用戶代碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

停頓時間越短就越適合需要與用戶交互或需要保證服務響應質量的程序,良好的響應速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的分析任務。

3)控制參數

  • 控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數,它允許的值是一個大於0的毫秒數,收集器將盡力保證內存回收花費的時間不超過用戶設定值。

  • 直接設置吞吐量大小的-XX:GCTimeRatio參數,它的值則應當是一個大於0小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。譬如把此參數設置爲19,那允許的最大垃圾收集時間就佔總時間的5%(即1/(1+19)),默認值爲99,即允許最大1%(即1/(1+99))的垃圾收集時間。

4)優點

-XX:+UseAdaptiveSizePolicy,這是一個開關參數,當這個參數被激活之後,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數。

虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。這種調節方式稱爲垃圾收集的自適應的調節策略(GC Ergonomics)。

如果我們對收集器運作不太瞭解,手工優化存在困難的話,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成也許是一個很不錯的選擇。

我們只需要把基本的內存數據設置好(如-Xmx設置最大堆),然後使用-XX:MaxGCPauseMillis參數(更關注最大停頓時間)或-XX:GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標,那具體細節參數的調節工作就由虛擬機完成了。

自適應調節策略也是Parallel Scavenge收集器區別於ParNew收集器的一個重要特性。

Serial Old收集器

1)簡要介紹

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。

2)使用

主要意義是供客戶端模式下的HotSpot虛擬機使用。

如果在服務端模式下,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作爲CMS收集器發生失敗時的後備預案,在併發收集發生Concurrent ModeFailure時使用。

Parallel Old收集器

1)簡要介紹

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法實現。

2)運行示意圖

在這裏插入圖片描述
上圖爲Parallel Scavenge(新生代)/Parallel Old(老年代)收集器運行圖

3)使用

這個收集器是直到JDK 6時纔開始提供的,在此之前,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外別無選擇,其他表現良好的老年代收集器,如CMS無法與它配合工作。

直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較爲稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。

CMS收集器

1)簡要介紹

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,也成爲“併發低停頓收集器”。

2)收集模式

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於標記-清除算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分爲四個步驟,包括:

  • 初始標記(CMS initial mark):標記一下GC Roots能直接關聯到的對象,速度很快;會發生“Stop The World”。

  • 併發標記(CMS concurrent mark):從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行。

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

  • 併發清除(CMS concurrent sweep):清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

3)運行示意圖

在這裏插入圖片描述
上圖爲Concurrent Mark Sweep收集器運行圖

4)優缺點

優點:

併發收集、低停頓。

缺點:

  • CMS收集器對處理器資源非常敏感

當處理器核心數量不足四個時,CMS對用戶程序的影響就可能變得很大。如果應用本來的處理器負載就很高,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然大幅降低。

  • 無法處理“浮動垃圾”(Floating Garbage),有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生

在CMS的併發標記和併發清理階段,用戶線程是還在繼續運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。

由於在垃圾收集階段用戶線程還需要持續運行,那就還需要預留足夠內存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集,必須預留一部分空間供併發收集時的程序運作使用。

在JDK 5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活;到了JDK 6時,CMS收集器的啓動閾值就已經默認提升至92%。

這面臨另一種風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“併發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啓動後備預案:凍結用戶線程的執行,臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,但這樣停頓時間就很長了。

  • 基於“標記-清除”算法實現的收集器,會有大量空間碎片產生

空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很多剩餘空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次Full GC的情況。

G1收集器

1)簡要介紹

Garbage First(簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向局部收集的設計思路和基於Region的內存佈局形式。

JDK9發佈之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成爲服務端模式下的默認垃圾收集器。

2)模式對比

在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標範圍要麼是整個新生代(Minor GC),要麼就是整個老年代(Major GC),再要麼就是整個Java堆(Full GC)。

而G1可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。

  • Region區域概念

G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。

  • Humongous區域概念

Region中一類特殊的區域,專門用來存儲大對象。G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍爲1MB~32MB,且應爲2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行爲都把Humongous Region作爲老年代的一部分來進行看待。

3)收集模式

G1收集器將Region作爲單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。

G1收集器會跟蹤各個Region裏面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(使用參數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。

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

4)運行示意圖

在這裏插入圖片描述
從上圖,我們可以看到G1收集器運行過程同樣分爲四個步驟:

  • 初始標記(Initial Marking):標記GC Roots能直接關聯到的對象,並且修改
    TAMS指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。
    這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。

  • 併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的對象。

  • 最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。

  • 篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成。

5)對比CMS收集器

優勢:

G1從整體來看是基於“標記-整理”算法實現的收集器,但從局部(兩個Region之間)上看又是基於“標記-複製”算法實現,無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,垃圾收集完成之後能提供規整的可用內存。

這種特性有利於程序長時間運行,在程序爲大對象分配內存時不容易因無法找到連續內存空間而提前觸發下一次收集。

劣勢:

就內存佔用來說,雖然G1和CMS都使用卡表來處理跨代指針,但G1的卡表實現更爲複雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導致G1的記憶集(和其他內存消耗)可能會佔整個堆容量的20%乃至更多的內存空間。

相比起來CMS的卡表就相當簡單,只有唯一一份,而且只需要處理老年代到新生代的引用,反過來則不需要,由於新生代的對象具有朝生夕滅的不穩定性,引用變化頻繁,能省下這個區域的維護開銷是很划算的。

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