java虛擬機(7)垃圾收集器 經典垃圾收集器 低延遲垃圾收集器

經典垃圾收集器

經典收集器之間的關係如圖,七種作用於不同分代的收集器。兩個收集器之間存在連線,就說明它們可以搭配使用。圖中收集器所處的區域,則表示它是屬於新生代收集器抑或是老年代收集器:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:CMS、Serial Old、Parallel Old
  • 整堆收集器: G1

名詞解釋

並行(Parallel)

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

併發(Concurrent)

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

吞吐量

吞吐量就是處理器用於運行用戶代碼的時間與處理器總消耗時間的比值,即:
吞吐量 = {運行用戶代碼的時間\over 運行用戶代碼的時間+運行垃圾手機時間}
如果虛擬機完成某個任務,用戶代碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

Serial收集器

新生代收集器,Serial收集器是最基礎、歷史最悠久的新生代收集器。

特點

簡單而高效。內存資源受限的環境,額外內存消耗(Memory Footprint)是最小的;單核處理器或處理器核心數較少的環境,沒有線程交互的開銷,可以獲得最高的單線程收集效率。

缺點

單線程工作,進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束(Stop The World)。

適用場景

客戶端模式下,如用戶桌面的應用場景以及部分微服務應用中。

ParNew收集器

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

特點

多線程,可使用處理器核心數量多時,垃圾回收效率高;默認開啓的收集線程數與處理器核心數量相同,可限制垃圾收集的線程數。

缺點

和Serial收集器一樣存在Stop The World問題。

適用場景

服務端模式下,JDK 7之前的遺留系統中首選的新生代收集器,原因是除了Serial收集器外,目前只有它能與CMS收集器配合工作。

Parallel Scavenge收集器

新生代收集器,與吞吐量關係密切,Parallel Scavenge收集器也經常被稱作“吞吐量優先收集器”。

特點

基於標記-複製算法;多線程,能夠並行收集;主要關注吞吐量,目標是達到一個可控制的吞吐量(Throughput);自適應調節策略。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量:

1、-XX:MaxGCPauseMillis參數,控制最大垃圾收集停頓時間;

一個大於0的毫秒數,收集器將盡力保證內存回收花費的時間不超過用戶設定值(垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間爲代價換取的)。

2、-XX:GCTimeRatio參數,直接設置吞吐量大小;

一個大於0小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。

自適應調節策略

開關參數 -XX:+UseAdaptiveSizePolicy,激活後不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

適用場景

高吞吐量則可以最高效率地利用處理器資源,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的分析任務。

Serial Old收集器

老年代收集器,Serial Old是Serial收集器的老年代版本。

特點

單線程收集器,使用標記-整理算法。

適用場景

供客戶端模式下的HotSpot虛擬機使用;服務端模式可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用;另外一種就是作爲CMS收集器發生失敗時的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本。

特點

支持多線程併發收集,基於標記-整理算法實現。

適用場景

在注重吞吐量或者處理器資源較爲稀缺的場合,都可以優先考慮Parallel Scavenge加ParallelOld收集器這個組合。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。

特點

以獲取最短回收停頓時間爲目標,基於標記-清除算法實現,併發收集、低停頓。

運作過程

  1. 初始標記(CMS initial mark):標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop TheWorld”;
  2. 併發標記(CMS concurrent mark):從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行;
  3. 重新標記(CMS remark):爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,停頓時間比初始標記階段稍長,比並發標記階段的時間遠遠短,需要“Stop TheWorld”
  4. 併發清除(CMS concurrent sweep):清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

由於在整個過程中耗時最長的併發標記和併發清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。

CMS收集器的運作步驟中併發和需要停頓的階段如圖:

適用場景

較爲關注服務的響應速度,希望系統停頓時間儘可能短,以給用戶帶來良好的交互體驗的應用,如集中在互聯網網站或者基於瀏覽器的B/S系統的服務端上的很大一部分Java應用。

缺點

  • 對CPU資源非常敏感,在併發階段,它雖然不會導致用戶線程停頓,但卻會因爲佔用了一部分線程(或者說處理器的計算能力)而導致應用程序變慢,降低總吞吐量;
  • 無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生;
  • 因爲採用標記-清除算法所以會存在空間碎片的問題,導致大對象無法分配空間,不得不提前觸發一次Full GC。

Garbage First收集器

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

適用場景

G1是一款主要面向服務端應用的垃圾收集器。

特點

  • Mixed GC模式:面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大。
  • 整體來看是基於“標記-整理”算法實現的收集器,局部(兩個Region之間)上看又是基於“標記-複製”算法實現,運作期間不會產生內存空間碎片,垃圾收集完成之後能提供規整的可用內存。
  • 可預測的停頓時間模型:將Region作爲單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。

G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。更具體的處理思路是讓G1收集器去跟蹤各個Region裏面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(使用參數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取儘可能高的收集效率。

將Java堆分成多個獨立Region後,Region裏面存在的跨Region引用對象如何解決?

使用記憶集避免全堆作爲GC Roots掃描,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,並標記這些指針分別在哪些卡頁的範圍之內。。

缺點:G1收集器要比其他的傳統垃圾收集器有着更高的內存佔用負擔,根據經驗,G1至少要耗費大約相當於Java堆容量10%至20%的額外內存來維持收集器工作。

在併發標記階段如何保證收集線程與用戶線程互不干擾地運行?

首先要解決的是用戶線程改變對象引用關係時,必須保證其不能打破原本的對象圖結構,導致標記結果出現錯誤,CMS收集器採用增量更新算法實現,而G1收集器則是通過原始快照(SATB)算法來實現的。

此外,垃圾收集對用戶線程的影響還體現在回收過程中新創建對象的內存分配上,程序要繼續運行就肯定會持續有新對象被創建,G1爲每一個Region設計了兩個名爲TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用於併發回收過程中的新對象分配,併發回收時新分配的對象地址都必須要在這兩個指針位置以上。G1收集器默認在這個地址以上的對象是被隱式標記過的,即默認它們是存活的,不納入回收範圍。與CMS中的“Concurrent Mode Failure”失敗會導致Full GC類似,如果內存回收的速度趕不上內存分配的速度,G1收集器也要被迫凍結用戶線程執行,導致Full GC而產生長時間“Stop TheWorld”。

怎樣建立起可靠的停頓預測模型?

用戶通過-XX:MaxGCPauseMillis參數指定的停頓時間只意味着垃圾收集發生之前的期望值,G1收集器的停頓預測模型是以衰減均值(DecayingAverage)爲理論基礎來實現的,在垃圾收集過程中,G1收集器會記錄每個Region的回收耗時、每個Region記憶集裏的髒卡數量等各個可測量的步驟花費的成本,並分析得出平均值、標準偏差、置信度等統計信息。

“衰減均值”是指它會比普通的平均值更容易受到新數據的影響,平均值代表整體平均狀態,但衰減平均值更準確地代表“最近的”平均狀態。Region的統計狀態越新越能決定其回收的價值,通過這些信息預測開始回收,由哪些Region組成回收集纔可以在不超過期望停頓時間的約束下獲得最高的收益。

運作過程

  1. 初始標記(Initial Marking):標記一下GC Roots能直接關聯到的對象,修改TAMS指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。需要停頓線程,但耗時很短,是借用進行Minor GC的時候同步完成的,所以在這個階段實際並沒有額外的停頓。
  2. 併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的對象。
  3. 最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。
  4. 篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成的。

低延遲垃圾收集器

Shenandoah和ZGC,幾乎整個工作過程全部都是併發的,只有初始標記、最終標記這些階段有短暫的停頓,這部分停頓的時間基本上是固定的,與堆的容量、堆中對象的數量沒有正比例關係。實際上,它們都可以在任意可管理的(譬如現在ZGC只能管理4TB以內的堆)堆容量下,實現垃圾收集的停頓都不超過十毫秒這種目標。這兩款目前仍處於實驗狀態的收集器,被官方命名爲“低延遲垃圾收集器”。

Shenandoah收集器

Shenandoah收集器是第一款由非Oracle開發的垃圾收集器,由RedHat公司獨立發展的新型收集器項目,在2014年RedHat把Shenandoah貢獻給了OpenJDK。是一款只有OpenJDK纔會包含,而OracleJDK裏反而不存在的收集器。

對比G1

和G1一樣,Shenandoah也是使用基於Region的堆內存佈局,同樣有着用於存放大對象的Humongous Region,默認的回收策略也同樣是優先處理回收價值最大的Region。

但在管理堆內存方面,它與G1至少有三個明顯的不同之處:

1、支持併發的整理算法,G1的回收階段是可以多線程並行的,但卻不能與用戶線程併發;

2、默認不使用分代收集的,不會有專門的新生代Region或者老年代Region的存在,出於性價比的權衡,基於工作量上的考慮而將其放到優先級較低的位置上。

3、Shenandoah摒棄了在G1中耗費大量內存和計算資源去維護的記憶集,改用名爲“連接矩陣”(Connection Matrix)的全局數據結構來記錄跨Region的引用關係,降低了處理跨代指針時的記憶集維護消耗,也降低了僞共享問題的發生概率。

連接矩陣可以簡單的理解爲一張二維表格,如果Region N有對象指向Region M,就在表格的N行M列中打上一個標記。在回收時通過這張表格就可以得出哪些Region 之間產生了跨代引用。

工作過程

  1. 初始標記(Initial Marking):與G1一樣,標記與GCRoots直接關聯的對象,這個階段仍是“Stop The World”的,但停頓時間與堆大小無關,只與GC Roots的數量相關。
  2. 併發標記(Concurrent Marking):與G1一樣,遍歷對象圖,標記出全部可達的對象,這個階段是與用戶線程一起併發的,時間長短取決於堆中存活對象的數量以及對象圖的結構複雜程度。
  3. 最終標記(Final Marking):與G1一樣,處理剩餘的SATB掃描,並在這個階段統計出回收價值最高的Region,將這些Region構成一組回收集(Collection Set)。最終標記階段也會有一小段短暫的停頓。
  4. 併發清理(Concurrent Cleanup):這個階段用於清理那些整個區域內連一個存活對象都沒有找到的Region(這類Region被稱爲Immediate Garbage Region)。
  5. 併發回收(Concurrent Evacuation):核心差異,在這個階段,Shenandoah要把回收集裏面的存活對象先複製一份到其他未被使用的Region之中,並通過讀屏障和被稱爲“Brooks Pointers”的轉發指針來解決,在移動對象的同時,用戶線程仍然可能不停對被移動的對象進行讀寫訪問,移動對象是一次性的行爲,但移動之後整個內存中所有指向該對象的引用都還是舊對象的地址的困難點。併發回收階段運行的時間長短取決於回收集的大小。
  6. 初始引用更新(Initial Update Reference):併發回收階段複製對象結束後,還需要把堆中所有指向舊對象的引用修正到複製後的新地址,這個操作稱爲引用更新。建立了一個線程集合點,確保所有併發回收階段中進行的收集器線程都已完成分配給它們的對象移動任務。初始引用更新時間很短,會產生一個非常短暫的停頓。
  7. 併發引用更新(Concurrent Update Reference):真正開始進行引用更新操作,這個階段是與用戶線程一起併發的,時間長短取決於內存中涉及的引用數量的多少。併發引用更新與併發標記不同,它不再需要沿着對象圖來搜索,只需要按照內存物理地址的順序,線性地搜索出引用類型,把舊值改爲新值即可。
  8. 最終引用更新(Final Update Reference):修正存在於GC Roots中的引用。這個階段是Shenandoah的最後一次停頓,停頓時間只與GC Roots的數量相關。
  9. 併發清理(Concurrent Cleanup):經過併發回收和引用更新之後,整個回收集中所有的Region已再無存活對象,這些Region都變成Immediate Garbage Regions了,最後再調用一次併發清理過程來回收這些Region的內存空間,供以後新對象分配使用。

三個最重要的併發節點:併發標記、併發回收、併發引用更新。

BrooksPointer

Shenandoah用以支持並行整理的核心概念——BrooksPointer轉發指針。

在原有對象佈局結構的最前面統一增加一個新的引用字段,在正常不處於併發移動的情況下,該引用指向對象自己(和句柄有一些相似,都是一種間接性的對象訪問方式,差別是句柄存儲在句柄池中,轉發指針是在每一個對象頭前面。)

當對象擁有了一份新的副本時,只需要修改一處指針的值,即舊對象上轉發指針的引用位置,使其指向新對象,便可將所有對該對象的訪問轉發到新的副本上。只要舊對象的內存仍然存在,未被清理掉,虛擬機內存中所有通過舊引用地址訪問的代碼便仍然可用,都會被自動轉發到新對象上繼續工作,如圖:

多線程競爭問題

通過比較並交換(Compare And Swap,CAS)操作來保證併發時對象的訪問正確性的。

缺點

數量龐大的讀屏障帶來的性能開銷,高運行負擔使得吞吐量下降。

ZGC收集器

ZGC(Z Garbage Collector),在JDK 11中新加入的具有實驗性質的低延遲垃圾收集器,由Oracle公司研發的,基於Region內存佈局的,(暫時)不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記-整理算法的,以低延遲爲首要目標的一款垃圾收集器。

內存佈局

ZGC的內存佈局與Shenandoah和G1一樣,ZGC也採用基於Region的堆內存佈局,但與它們不同的是,ZGC的Region具有動態性——動態創建和銷燬,以及動態的區域容量大小。

在x64硬件平臺下,ZGC的Region可以具有大、中、小三類容量:

  • 小型Region(Small Region):容量固定爲2MB,用於放置小於256KB的小對象。
  • 中型Region(Medium Region):容量固定爲32MB,用於放置大於等於256KB但小於4MB的對象。
  • 大型Region(Large Region):容量不固定,可以動態變化,但必須爲2MB的整數倍,用於放置4MB或以上的大對象。每個大型Region中只會存放一個大對象。

併發整理算法的實現

Shenandoah使用轉發指針和讀屏障來實現併發整理,ZGC雖然同樣用到了讀屏障,但與Shenandoah完全不同,它採用的染色指針技術。

染色指針是一種直接將少量額外的信息存儲在指針上的技術,ZGC的染色指針直接把標記信息記在引用對象的指針上。

Linux下64位指針的高18位不能用來尋址,但剩餘的46位指針所能支持的64TB內存仍夠充分滿足大型服務器的需要。ZGC的染色指針技術即利用剩下的46位指針寬度,將其高4位提取出來存儲四個標誌信息。通過這些標誌位,虛擬機可以直接從指針中看到其引用對象的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。由於這些標誌位壓縮了原本就只有46位的地址空間,也直接導致ZGC能夠管理的內存不可以超過4TB(2的42次冪)。

染色指針的三大優勢

1、染色指針可以使得一旦某個Region的存活對象被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正後才能清理。

2、染色指針可以大幅減少在垃圾收集過程中內存屏障的使用數量,設置內存屏障,尤其是寫屏障的目的通常是爲了記錄對象引用的變動情況,如果將這些信息直接維護在指針中,就可以省去一些專門的記錄操作。

3、染色指針可以作爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便日後進一步提高性能。

多重映射

Linux/x86-64平臺上的ZGC使用了多重映射(Multi-Mapping)將多個不同的虛擬內存地址映射到同一個物理內存地址上,這是一種多對一映射,意味着ZGC在虛擬內存中看到的地址空間要比實際的堆內存容量來得更大。把染色指針中的標誌位看作是地址的分段符,那隻要將這些不同的地址段都映射到同一個物理內存空間,經過多重映射轉換後,就可以使用染色指針正常進行尋址了,如圖:

運作過程

ZGC的運作過程大致可劃分爲以下四個大的階段:

  1. 併發標記(Concurrent Mark):與G1、Shenandoah一樣,併發標記是遍歷對象圖做可達性分析的階段,前後也要經過類似於G1、Shenandoah的初始標記、最終標記的短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的。

  2. 併發預備重分配(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(RelocationSet)。此外,在JDK 12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個階段中完成的。

  3. 併發重分配(Concurrent Relocate):重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活對象複製到新的Region上,併爲重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關係。

    指針的“自愈”(Self-Healing)能力

    得益於染色指針,ZGC收集器能僅從引用上就明確得知一個對象是否處於重分配集之中,如果用戶線程此時併發訪問了位於重分配集中的對象,這次訪問將會被預置的內存屏障所截獲,然後立即根據Region上的轉發表記錄將訪問轉發到新複製的對象上,並同時修正更新該引用的值,使其直接指向新對象。

    優點

    1、只有第一次訪問舊對象會陷入轉發,也就是隻慢一次,對比Shenandoah的Brooks轉發指針,每次對象訪問都必須付出的固定開銷,ZGC對用戶程序的運行時負載要低一些。

    2、一旦重分配集中某個Region的存活對象都複製完畢後,這個Region就可以立即釋放用於新對象的分配(轉發表除外),即使堆中還有很多指向這個對象的未更新指針也沒有關係,這些舊指針一旦被使用,它們都是可以自愈的。

  4. 併發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用,這一點從目標角度看是與Shenandoah併發引用更新階段一樣的。ZGC很巧妙地把併發重映射階段要做的工作,合併到了下一次垃圾收集循環中的併發標記階段裏去完成,節省了一次遍歷對象圖的開銷。一旦所有指針都被修正之後,原來記錄新舊對象關係的轉發表就可以釋放掉了。

浮動垃圾產生

一次完整的併發收集,假設全過程要持續十分鐘以上,在這段時間裏面,由於應用的對象分配速率很高,將創造大量的新對象,新對象很難進入當次收集的標記範圍,就只能全部當作存活對象來看待,但其中絕大部分對象都是朝生夕滅的,這就產生了大量的浮動垃圾。

目前唯一的辦法就是儘可能地增加堆容量大小,獲得更多喘息的時間。但若要從根本上提升ZGC能夠應對的對象分配速率,還是需要引入分代收集,讓新生對象都在一個專門的區域中創建,然後專門針對這個區域進行更頻繁、更快的收集。

“NUMA-Aware”的內存分配

NUMA(Non-Uniform MemoryAccess,非統一內存訪問架構)是一種爲多處理器或者多核處理器的計算機所設計的內存架構。

在NUMA架構下,ZGC收集器會優先嚐試在請求線程當前所處的處理器的本地內存上分配對象,保證高效的內存訪問。ZGC之前的收集器就只有針對吞吐量設計的ParallelScavenge支持NUMA內存分配,如今ZGC也成爲另外一個選擇。

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