天天看點

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

學習了垃圾收集算法與垃圾收集器,現在把學習筆記總結記錄一下,如果記錄有些錯誤,還望指出。

文章目錄

  • 一、垃圾收集算法
    • 1.分代收集理論
    • 2.标記-複制算法
    • 3.标記-清除算法
    • 4.标記-整理算法
  • 二、垃圾收集器
    • 1.Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
    • 2.Parallel Scavenge收集器(-XX:+UseParallelGC(年輕代),-XX:+UseParallelOldGC(老年代))
    • 3.ParNew收集器(-XX:+UseParNewGC)
    • 4.CMS收集器(-XX:+UseConcMarkSweepGC(old))
    • 5.垃圾收集底層算法實作
    • 6.G1收集器(-XX:+UseG1GC)
    • 7.ZGC收集器(-XX:+UseZGC)
  • 三、如何選擇垃圾收集器

一、垃圾收集算法

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

1.分代收集理論

目前虛拟機的垃圾收集都采用分代收集算法,這種算法沒有什麼新的思想,隻是根據對象存活周期的不同将記憶體分為幾 塊。一般将java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。 比如在新生代中,每次收集都會有大量對象(近99%)死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可 以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行配置設定擔保,是以我們必須選 擇“标記-清除”或“标記-整理”算法進行垃圾收集。注意,“标記-清除”或“标記-整理”算法會比複制算法慢10倍以 上。

2.标記-複制算法

為了解決效率問題,“複制”收集算法出現了。它可以将記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的 記憶體使用完後,就将還存活的對象複制到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對 記憶體區間的一半進行回收。

一個明顯的問題:

  1. 空間浪費問題(另外一半永遠是空着的比較占用記憶體)
垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

3.标記-清除算法

算法分為“标記”和“清除”階段:标記存活的對象, 統一回收所有未被标記的對象(一般選擇這種);也可以反過來,标 記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象 。它是最基礎的收集算法,比較簡單,但是會帶來

兩個明顯的問題:

  1. 效率問題 (如果需要标記的對象太多,效率不高)
  2. 空間問題(标記清除後會産生大量不連續的碎片)
垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

4.标記-整理算法

根據老年代的特點特出的一種标記算法,标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象回 收,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

二、垃圾收集器

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

如果說收集算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實作。

雖然我們對各個收集器進行比較,但并非為了挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出 現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇适合自己的垃圾收集器。試想一下:如果有一 種四海之内、任何場景下都适用的完美收集器存在,那麼我們的Java虛拟機就不會實作那麼多不同的垃圾收集器了。

1.Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本、曆史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它 的 “單線程” 的意義不僅僅意味着它隻會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工 作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

虛拟機的設計者們當然知道Stop The World帶來的不良使用者體驗,是以在後續的垃圾收集器設計中停頓時間在不斷縮短 (仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是Serial收集器有沒有優于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由于沒有線程互動的開銷,自然可以獲得很高的單線程收集效率。

Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5 以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的後備方案。

2.Parallel Scavenge收集器(-XX:+UseParallelGC(年輕代),-XX:+UseParallelOldGC(老年代))

Parallel收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算 法、回收政策等等)和Serial收集器類似。預設的收集線程數跟cpu核數相同,當然也可以用參數(- XX:ParallelGCThreads)指定收集線程數,但是一般不推薦修改。

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是使用者線程的停 頓時間(提高使用者體驗)。所謂吞吐量就是CPU中用于運作使用者代碼的時間與CPU總消耗時間的比值。 Parallel

Scavenge收集器提供了很多參數供使用者找到最合适的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,可以 選擇把記憶體管理優化交給虛拟機去完成也是一個不錯的選擇。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多線程和“标記-整理”算法。在注重吞吐量以及 CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器(JDK8預設的新生代和老年代收集 器)。

3.ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其實跟Parallel收集器很類似,差別主要在于它可以和CMS收集器配合使用。

新生代采用複制算法,老年代采用标記-整理算法。

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

它是許多運作在Server模式下的虛拟機的首要選擇,除了Serial收集器外,隻有它能與CMS收集器(真正意義上的并發收 集器,後面會介紹到)配合工作。

4.CMS收集器(-XX:+UseConcMarkSweepGC(old))

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。它非常符合在注重使用者體 驗的應用上使用,它是HotSpot虛拟機第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集線程與使用者線程 (基本上)同時工作

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “标記-清除”算法實作的,它的運作過程相比于前面 幾種垃圾收集器來說更加複雜一些。整個過程分為四個步驟:

  • 初始标記: 暫停所有的其他線程(STW),并記錄下gc roots直接能引用的對象,速度很快。
  • 并發标記: 并發标記階段就是從GC Roots的直接關聯對象開始周遊整個對象圖的過程, 這個過程耗時較長但 是不需要停頓使用者線程, 可以與垃圾收集線程一起并發運作。因為使用者程式繼續運作,可能會有導緻已經标記過的

    對象狀态發生改變。

  • 重新标記: 重新标記階段就是為了修正并發标記期間因為使用者程式繼續運作而導緻标記産生變動的那一部分對 象的标記記錄,這個階段的停頓時間一般會比初始标記階段的時間稍長,遠遠比并發标記階段時間短。主要用到三 色标記裡的增量更新算法(見下面詳解)做重新标記。
  • 并發清理: 開啟使用者線程,同時GC線程開始對未标記的區域做清掃。這個階段如果有新增對象會被标記為黑 色不做任何處理(見下面三色标記算法詳解)。
  • 并發重置:重置本次GC過程中的标記資料。
    垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

    從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:并發收集、低停頓。

    但是它有下面幾個明顯的缺點:

  • 對CPU資源敏感(會和服務搶資源);
  • 無法處理浮動垃圾(在并發标記和并發清理階段又産生垃圾,這種浮動垃圾隻能等到下一次gc再清理了);
  • 它使用的回收算法-“标記-清除”算法會導緻收集結束時會有大量空間碎片産生,當然通過參數- XX:+UseCMSCompactAtFullCollection可以讓jvm在執行完标記清除後再做整理
  • 執行過程中的不确定性,會存在上一次垃圾回收還沒執行完,然後垃圾回收又被觸發的情況,特别是在并 發标記和并發清理階段會出現,一邊回收,系統一邊運作,也許沒回收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收

CMS的相關核心參數

  1. -XX:+UseConcMarkSweepGC:啟用cms
  2. -XX:ConcGCThreads:并發的GC線程數
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之後做壓縮整理(減少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之後壓縮一次,預設是0,代表每次FullGC後都會壓縮一 次
  5. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(預設是92,這是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:隻使用設定的回收門檻值(-XX:CMSInitiatingOccupancyFraction設 定的值),如果不指定,JVM僅在第一次使用設定值,後續則會自動調整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在于減少老年代對年輕代的引 用,降低CMS GC的标記階段時的開銷,一般CMS的GC耗時 80%都在标記階段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标記的時候多線程執行,縮短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标記的時候多線程執行,縮短STW;

5.垃圾收集底層算法實作

三色标記

在并發标記的過程中,因為标記期間應用線程還在繼續跑,對象間的引用可能發生變化,多标和漏标的情況就有可能發生。 這裡我們引入“三色标記”來給大家解釋下,把Gcroots可達性分析周遊對象過程中遇到的對象, 按照“是否通路過”這個條件标記成以 下三種顔色:

  • 黑色: 表示對象已經被垃圾收集器通路過, 且這個對象的所有引用都已經掃描過。 黑色的對象代表已經掃描 過, 它是安全存活的, 如果有其他對象引用指向了黑色對象, 無須重新掃描一遍。 黑色對象不可能直接(不經過 灰色對象) 指向某個白色對象。
  • 灰色: 表示對象已經被垃圾收集器通路過, 但這個對象上至少存在一個引用還沒有被掃描過。
  • 白色: 表示對象尚未被垃圾收集器通路過。 顯然在可達性分析剛剛開始的階段, 所有的對象都是白色的, 若 在分析結束的階段, 仍然是白色的對象, 即代表不可達。
垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

多标-浮動垃圾

在并發标記過程中,如果由于方法運作結束導緻部分局部變量(gcroot)被銷毀,這個gcroot引用的對象之前又被掃描過 (被标記為非垃圾對象),那麼本輪GC不會回收這部分記憶體。這部分本應該回收但是沒有回收到的記憶體,被稱之為“浮動 垃圾”。浮動垃圾并不會影響垃圾回收的正确性,隻是需要等到下一輪垃圾回收中才被清除。

另外,針對并發标記(還有并發清理)開始後産生的新對象,通常的做法是直接全部當成黑色,本輪不會進行清除。這部分 對象期間可能也會變為垃圾,這也算是浮動垃圾的一部分。

漏标-讀寫屏障

漏标會導緻被引用的對象被當成垃圾誤删除,這是嚴重bug,必須解決,有兩種解決方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)。

  • 增量更新就是當黑色對象插入新的指向白色對象的引用關系時, 就将這個新插入的引用記錄下來, 等并發掃描結束之 後, 再将這些記錄過的引用關系中的黑色對象為根, 重新掃描一次。 這可以簡化了解為, 黑色對象一旦新插入了指向 白色對象的引用之後,它就變回灰色對象了。
  • 原始快照就是當灰色對象要删除指向白色對象的引用關系時, 就将這個要删除的引用記錄下來, 在并發掃描結束之後, 再将這些記錄過的引用關系中的灰色對象為根, 重新掃描一次,這樣就能掃描到白色的對象,将白色對象直接标記為黑

    色(目的就是讓這種對象在本輪gc清理中能存活下來,待下一輪gc的時候重新掃描,這個對象也有可能是浮動垃圾)

    以上無論是對引用關系記錄的插入還是删除, 虛拟機的記錄操作都是通過寫屏障實作的。

寫屏障

給某個對象的成員變量指派時,其底層代碼大概長這樣:

/**
 *  @param field 某對象的成員變量,如 a.b.d
 *  @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) {
	*field = new_value; // 指派操作
}
           

所謂的寫屏障,其實就是指在指派操作前後,加入一些處理(可以參考AOP的概念):

void oop_field_store(oop* field, oop new_value) {
	pre_write_barrier(field); // 寫屏障‐寫前操作
	*field = new_value;
	post_write_barrier(field, value); // 寫屏障‐寫後操作
}
           

寫屏障實作SATB

當對象B的成員變量的引用發生變化時,比如引用消失(a.b.d = null),我們可以利用寫屏障,将B原來成員變量的引用 對象D記錄下來:

void pre_write_barrier(oop* field) {
 	oop old_value = *field; // 擷取舊值
 	remark_set.add(old_value); // 記錄原來的引用對象
 }
           

寫屏障實作增量更新

當對象B的成員變量的引用發生變化時,比如引用消失(a.b.d = null),我們可以利用寫屏障,将B原來成員變量的引用 對象D記錄下來:

void post_write_barrier(oop* field, oop new_value) {
 	remark_set.add(new_value); // 記錄新引用的對象
 }
           

讀屏障

給某個對象的成員變量指派時,其底層代碼大概長這樣:

oop oop_field_load(oop* field) {
	pre_load_barrier(field); // 讀屏障‐讀取前操作
	return *field;
}
           

讀屏障是直接針對第一步:D d = a.b.d,當讀取成員變量時,一律記錄下來:

void pre_load_barrier(oop* field) {
	oop old_value = *field;
	remark_set.add(old_value); // 記錄讀取到的對象
}
           

現代追蹤式(可達性分析)的垃圾回收器幾乎都借鑒了三色标記的算法思想,盡管實作的方式不盡相同:比如白色/黑色 集合一般都不會出現(但是有其他展現顔色的地方)、灰色集合可以通過棧/隊列/緩存日志等方式進行實作、周遊方式可 以是廣度/深度周遊等等。

對于讀寫屏障,以Java HotSpot VM為例,其并發标記時對漏标的處理方案如下:

  • CMS:寫屏障 + 增量更新
  • G1,Shenandoah:寫屏障 + SATB
  • ZGC:讀屏障

工程實作中,讀寫屏障還有其他功能,比如寫屏障可以用于記錄跨代/區引用的變化,讀屏障可以用于支援移動對象的并 發執行等。功能之外,還有性能的考慮,是以對于選擇哪種,每款垃圾回收器都有自己的想法。

為什麼G1用SATB?CMS用增量更新?

我的了解:SATB相對增量更新效率會高(當然SATB可能造成更多的浮動垃圾),因為不需要在重新标記階段再次深度掃描 被删除引用對象,而CMS對增量引用的根對象會做深度掃描,G1因為很多對象都位于不同的region,CMS就一塊老年代 區域,重新深度掃描對象的話G1的代價會比CMS高,是以G1選擇SATB不深度掃描對象,隻是簡單标記,等到下一輪GC 再深度掃描。

6.G1收集器(-XX:+UseG1GC)

G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足GC 停頓時間要求的同時,還具備高吞吐量性能特征.

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

G1将Java堆劃分為多個大小相等的獨立區域(Region),JVM最多可以有2048個Region。

一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當然也可以用參數"- XX:G1HeapRegionSize"手動指定Region大小,但是推薦預設的計算方式。

G1保留了年輕代和老年代的概念,但不再是實體隔閡了,它們都是(可以不連續)Region的集合。

預設年輕代對堆記憶體的占比是5%,如果堆大小為4096M,那麼年輕代占據200MB左右的記憶體,對應大概是100個 Region,可以通過“-XX:G1NewSizePercent”設定新生代初始占比,在系統運作中,JVM會不停的給年輕代增加更多 的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”調整。年輕代中的Eden和 Survivor對應的region也跟之前一樣,預設8:1:1,假設年輕代現在有1000個region,eden區對應800個,s0對應100 個,s1對應100個。

一個Region可能之前是年輕代,如果Region進行了垃圾回收,之後可能又會變成老年代,也就是說Region的區域功能 可能會動态變化。

G1垃圾收集器對于對象什麼時候會轉移到老年代跟之前講過的原則一樣,唯一不同的是對大對象的處理,G1有專門配置設定 大對象的Region叫Humongous區,而不是讓大對象直接進入老年代的Region中。在G1中,大對象的判定規則就是一 個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,隻要一個大對象超過了1M,就會被放 入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。

Humongous區專門存放短期巨型對象,不用直接進老年代,可以節約老年代的空間,避免因為老年代空間不夠的GC開 銷。

Full GC的時候除了收集年輕代和老年代之外,也會将Humongous區一并回收。

G1收集器一次GC的運作過程大緻分為以下幾個步驟:

  • 初始标記(initial mark,STW):暫停所有的其他線程,并記錄下gc roots直接能引用的對象,速度很快;
  • 并發标記(Concurrent Marking):同CMS的并發标記
  • 最終标記(Remark,STW):同CMS的重新标記
  • 篩選回收(Cleanup,STW):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期 望的GC停頓時間(可以用JVM參數 -XX:MaxGCPauseMillis指定)來制定回收計劃,比如說老年代此時有1000個 Region都滿了,但是因為根據預期停頓時間,本次垃圾回收可能隻能停頓200毫秒,那麼通過之前回收成本計算得 知,可能回收其中800個Region剛好需要200ms,那麼就隻會回收800個Region(Collection Set,要回收的集 合),盡量把GC導緻的停頓時間控制在我們指定的範圍内。這個階段其實也可以做到與使用者程式一起并發執行,但 是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率。不管是年輕代或是老 年代,回收算法主要用的是複制算法,将一個region中的存活對象複制到另一個region中,這種不會像CMS那樣 回收完因為有很多記憶體碎片還需要整理一次,G1采用複制算法回收幾乎不會有太多記憶體碎片。(注意:CMS回收階 段是跟使用者線程一起并發執行的,G1因為内部實作太複雜暫時沒實作并發回收,不過到了Shenandoah就實作了并 發收集,Shenandoah可以看成是G1的更新版本)
    垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器
    G1收集器在背景維護了一個優先清單,每次根據允許的收集時間,優先選擇回收價值最大Region(這也就是它的名字 Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回 收時間有限情況下,G1當然會優先選擇後面這個Region回收。這種使用Region劃分記憶體空間以及有優先級的區域回收 方式,保證了G1收集器在有限時間内可以盡可能高的收集效率。

被視為JDK1.7以上版本Java虛拟機的一個重要進化特征。它具備以下特點:

  • 并行與并發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop- The-World停頓時間。部分其他收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式 讓java程式繼續執行。
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念
  • 空間整合:與CMS的“标記–清理”算法不同,G1從整體來看是基于“标記整理”算法實作的收集器;從局部 上來看是基于“複制”算法實作的。
  • 可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了 追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段(通過參數"- XX:MaxGCPauseMillis"指定)内完成垃圾收集。

毫無疑問, 可以由使用者指定期望的停頓時間是G1收集器很強大的一個功能, 設定不同的期望停頓時間, 可使得G1在不 同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這裡設定的“期望值”必須是符合實際的, 不能異想 天開, 畢竟G1是要當機使用者線程來複制對象的, 這個停頓時 間再怎麼低也得有個限度。 它預設的停頓目标為兩百毫秒, 一般來說, 回收階段占到幾十到一百甚至接近兩百毫秒都很 正常, 但如果我們把停頓時間調得非常低, 譬如設定為二十毫秒, 很可能出現的結果就是由于停頓目标時間太短, 導 緻每次選出來的回收集隻占堆記憶體很小的一部分, 收集器收集的速度逐漸跟不上配置設定器配置設定的速度, 導緻垃圾慢慢堆 積。 很可能一開始收集器還能從空閑的堆記憶體中獲得一些喘息的時間, 但應用運作時間一長就不行了, 最終占滿堆引發 Full GC反而降低性能, 是以通常把期望停頓時間設定為一兩百毫秒或者兩三百毫秒會是比較合理的。

G1垃圾收集分類

YoungGC

YoungGC并不是說現有的Eden區放滿了就會馬上觸發,G1會計算下現在Eden區回收大概要多久時間,如果回收時 間遠遠小于參數 -XX:MaxGCPauseMills 設定的值,那麼增加年輕代的region,繼續給新對象存放,不會馬上做Young GC,直到下一次Eden區放滿,G1計算回收時間接近參數 -XX:MaxGCPauseMills 設定的值,那麼就會觸發Young GC

MixedGC

不是FullGC,老年代的堆占有率達到參數(-XX:InitiatingHeapOccupancyPercent)設定的值則觸發,回收所有的 Young和部分Old(根據期望的GC停頓時間确定old區垃圾收集的優先順序)以及大對象區,正常情況G1的垃圾收集是先做 MixedGC,主要使用複制算法,需要把各個region中存活的對象拷貝到别的region裡去,拷貝過程中如果發現沒有足夠 的空region能夠承載拷貝對象就會觸發一次Full GC

Full GC

停止系統程式,然後采用單線程進行标記、清理和壓縮整理,好空閑出來一批Region來供下一次MixedGC使用,這 個過程是非常耗時的。(Shenandoah優化成多線程收集了)

G1收集器參數設定

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的線程數量

-XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的N次幂),預設将整堆劃分為2048個分區

-XX:MaxGCPauseMillis:目标暫停時間(預設200ms)

-XX:G1NewSizePercent:新生代記憶體初始空間(預設整堆5%)

-XX:G1MaxNewSizePercent:新生代記憶體最大空間

-XX:TargetSurvivorRatio:Survivor區的填充容量(預設50%),Survivor區域裡的一批對象(年齡1+年齡2+年齡n的多個 年齡對象)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代

-XX:MaxTenuringThreshold:最大年齡門檻值(預設15)

-XX:InitiatingHeapOccupancyPercent:老年代占用空間達到整堆記憶體門檻值(預設45%),則執行新生代和老年代的混合 收集(MixedGC),比如我們之前說的堆預設有2048個region,如果有接近1000個region都是老年代的region,則可能 就要觸發MixedGC了

-XX:G1MixedGCLiveThresholdPercent(預設85%) region中的存活對象低于這個值時才會回收該region,如果超過這 個值,存活對象過多,回收的的意義不大。

-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(預設8次),在最後一個篩選回收階段可以回收一 會,然後暫停回收,恢複系統運作,一會再開始回收,這樣可以讓系統不至于單次停頓時間過長。

-XX:G1HeapWastePercent(預設5%): gc過程中空出來的region是否充足門檻值,在混合回收的時候,對Region回收都 是基于複制算法進行的,都是把要回收的Region裡的存活對象放入其他Region,然後這個Region中的垃圾對象全部清 理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閑出來的Region數量達到了堆記憶體的5%,此時就會立 即停止混合回收,意味着本次混合回收就結束了。

G1垃圾收集器優化建議

假設參數 -XX:MaxGCPauseMills 設定的值很大,導緻系統運作很久,年輕代可能都占用了堆記憶體的60%了,此時才 觸發年輕代gc。

那麼存活下來的對象可能就會很多,此時就會導緻Survivor區域放不下那麼多的對象,就會進入老年代中。

或者是你年輕代gc過後,存活下來的對象過多,導緻進入Survivor區域後觸發了動态年齡判定規則,達到了Survivor 區域的50%,也會快速導緻一些對象進入老年代中。

是以這裡核心還是在于調節 -XX:MaxGCPauseMills 這個參數的值,在保證他的年輕代gc别太頻繁的同時,還得考慮 每次gc過後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc.

什麼場景适合使用G1

  1. 50%以上的堆被存活對象占用
  2. 對象配置設定和晉升的速度變化非常大
  3. 垃圾回收時間特别長,超過1秒
  4. 8GB以上的堆記憶體(建議值)
  5. 停頓時間是500ms以内

7.ZGC收集器(-XX:+UseZGC)

ZGC是一款JDK 11中新加入的具有實驗性質的低延遲垃圾收集器,ZGC可以說源自于是Azul System公司開發的 C4(Concurrent Continuously Compacting Collector) 收集器。

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

ZGC目标

如下圖所示,ZGC的目标主要有4個:

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器
  • 支援TB量級的堆。我們生産環境的硬碟還沒有上TB呢,這應該可以滿足未來十年内,所有JAVA應用的需求了 吧
  • 最大GC停頓時間不超10ms。目前一般線上環境運作良好的JAVA應用Minor GC停頓時間在10ms左右, Major GC一般都需要100ms以上(G1可以調節停頓時間,但是如果調的過低的話,反而會适得其反),之是以能 做到這一點是因為它的停頓時間主要跟Root掃描有關,而Root數量和堆大小是沒有任何關系的。
  • 奠定未來GC特性的基礎。
  • 最糟糕的情況下吞吐量會降低15%。這都不是事,停頓時間足夠優秀。至于吞吐量,通過擴容分分鐘解決。 另外,Oracle官方提到了它最大的優點是:它的停頓時間不會随着堆的增大而增長!也就是說,幾十G堆的停頓時間是 10ms以下,幾百G甚至上T堆的停頓時間也是10ms以下。

不分代(暫時)

單代,即ZGC「沒有分代」。我們知道以前的垃圾回收器之是以分代,是因為源于“「大部分對象朝生夕死」”的假 設,事實上大部分系統的對象配置設定行為也确實符合這個假設。

那麼為什麼ZGC就不分代呢?因為分代實作起來麻煩,作者就先實作出一個比較簡單可用的單代版本,後續會優化。

ZGC記憶體布局

ZGC收集器是一款基于Region記憶體布局的, 暫時不設分代的, 使用了讀屏障、 顔色指針等技術來實作可并發的标記-整 理算法的, 以低延遲為首要目标的一款垃圾收集器。

ZGC的Region可以具有如圖3-19所示的大、 中、 小三類容量:

  • 小型Region(Small Region) : 容量固定為2MB, 用于放置小于256KB的小對象。
  • 中型Region(Medium Region) : 容量固定為32MB, 用于放置大于等于256KB但小于4MB的對象。
  • 大型Region(Large Region) : 容量不固定, 可以動态變化, 但必須為2MB的整數倍, 用于放置4MB或 以上的大對象。 每個大型Region中
  • 隻會存放一個大對象, 這也預示着雖然名字叫作“大型Region”, 但它的實際容量完全有可能小于中型 Region, 最小容量可低至4MB。 大型Region在ZGC的實作中是不會被重配置設定(重配置設定是ZGC的一種處理動作, 用于複制對象的收集器階段, 稍後會介紹到)的, 因為複制一個大對象的代價非常高昂。
垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

NUMA-aware

NUMA對應的有UMA,UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture。UMA表示記憶體隻有一塊,所有CPU都去通路這一塊記憶體,那麼就會存在競争問題(争奪記憶體總線通路 權),有競争就會有鎖,有鎖效率就會受到影響,而且CPU核心數越多,競争就越激烈。NUMA的話每個CPU對應有一 塊記憶體,且這塊記憶體在主機闆上離這個CPU是最近的,每個CPU優先通路這塊記憶體,那效率自然就提高了:

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

伺服器的NUMA架構在中大型系統上一直非常盛行,也是高性能的解決方案,尤其在系統延遲方面表現都很優秀。ZGC 是能自動感覺NUMA架構并充分利用NUMA架構特性的。

ZGC運作過程

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

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器
  • 并發标記(Concurrent Mark):與G1一樣,并發标記是周遊對象圖做可達性分析的階段,它的初始标記 (Mark Start)和最終标記(Mark End)也會出現短暫的停頓,與G1不同的是, ZGC的标記是在指針上而不是在對象 上進行的, 标記階段會更新染色指針中的Marked 0、 Marked 1标志位。
  • 并發預備重配置設定(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收 集過程要清理哪些Region,将這些Region組成重配置設定集(Relocation Set)。ZGC每次回收都會掃描所有的 Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。
  • 并發重配置設定(Concurrent Relocate):重配置設定是ZGC執行過程中的核心階段,這個過程要把重配置設定集中的存 活對象複制到新的Region上,并為重配置設定集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象 到新對象的轉向關系。ZGC收集器能僅從引用上就明确得知一個對象是否處于重配置設定集之中,如果使用者線程此時并 發通路了位于重配置設定集中的對象,這次通路将會被預置的記憶體屏障(讀屏障)所截獲,然後立即根據Region上的轉發 表記錄将通路轉發到新複制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC将這種行為稱為指 針的“自愈”(Self-Healing)能力。

1 ZGC的顔色指針因為“自愈”(Self‐Healing)能力,是以隻有第一次通路舊對象會變慢, 一旦重配置設定集中某個Region的存活對象都複制完畢 後,

2 這個Region就可以立即釋放用于新對象的配置設定,但是轉發表還得留着不能釋放掉, 因為可能還有通路在使用這個轉發表。

  • 并發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重配置設定集中舊對象的所有引用,但 是ZGC中對象引用存在“自愈”功能,是以這個重映射操作并不是很迫切。ZGC很巧妙地把并發重映射階段要做的 工作,合并到了下一次垃圾收集循環中的并發标記階段裡去完成,反正它們都是要周遊所有對象的,這樣合并就節 省了一次周遊對象圖的開銷。一旦所有指針都被修正之後, 原來記錄新舊對象關系的轉發表就可以釋放掉了。

顔色指針

Colored Pointers,即顔色指針,如下圖所示,ZGC的核心設計之一。以前的垃圾回收器的GC資訊都儲存在對象頭中, 而ZGC的GC資訊儲存在指針中。

垃圾收集算法與垃圾收集器一、垃圾收集算法二、垃圾收集器三、如何選擇垃圾收集器

每個對象有一個64位指針,這64位被分為:

  • 18位:預留給以後使用;
  • 1位:Finalizable辨別,此位與并發引用處理有關,它表示這個對象隻能通過finalizer才能通路;
  • 1位:Remapped辨別,設定此位的值後,對象未指向relocation set中(relocation set表示需要GC的 Region集合);
  • 1位:Marked1辨別;
  • 1位:Marked0辨別,和上面的Marked1都是标記對象用于輔助GC;
  • 42位:對象的位址(是以它可以支援2^42=4T記憶體):

顔色指針的三大優勢:

  1. 一旦某個Region的存活對象被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指 向該Region的引用都被修正後才能清理,這使得理論上隻要還有一個空閑Region,ZGC就能完成收集。
  2. 顔色指針可以大幅減少在垃圾收集過程中記憶體屏障的使用數量,ZGC隻使用了讀屏障。
  3. 顔色指針具備強大的擴充性,它可以作為一種可擴充的存儲結構用來記錄更多與對象标記、重定位過程相關的數 據,以便日後進一步提高性能。

ZGC存在的問題

ZGC最大的問題是浮動垃圾。ZGC的停頓時間是在10ms以下,但是ZGC的執行時間還是遠遠大于這個時間的。假如ZGC 全過程需要執行10分鐘,在這個期間由于對象配置設定速率很高,将建立大量的新對象,這些對象很難進入當次GC,是以隻 能在下次GC的時候進行回收,這些隻能等到下次GC才能回收的對象就是浮動垃圾。

1 ZGC沒有分代概念,每次都需要進行全堆掃描,導緻一些“朝生夕死”的對象沒能及時的被回收。

解決方案

目前唯一的辦法是增大堆的容量,使得程式得到更多的喘息時間,但是這個也是一個治标不治本的方案。如果需要從根 本上解決這個問題,還是需要引入分代收集,讓新生對象都在一個專門的區域中建立,然後專門針對這個區域進行更頻 繁、更快的收集。

ZGC觸發時機

ZGC目前有4中機制觸發GC:

  • 定時觸發,預設為不使用,可通過ZCollectionInterval參數配置。
  • 預熱觸發,最多三次,在堆記憶體達到10%、20%、30%時觸發,主要時統計GC時間,為其他GC機制使用。
  • 配置設定速率,基于正态分布統計,計算記憶體99.9%可能的最大配置設定速率,以及此速率下記憶體将要耗盡的時間點, 在耗盡之前觸發GC(耗盡時間 - 一次GC最大持續時間 - 一次GC檢測周期時間)。
  • 主動觸發,(預設開啟,可通過ZProactive參數配置) 距上次GC堆記憶體增長10%,或超過5分鐘時,對比距上 次GC的間隔時間跟(49 * 一次GC的最大持續時間),超過則觸發。

三、如何選擇垃圾收集器

  1. 優先調整堆的大小讓伺服器自己來選擇
  2. 如果記憶體小于100M,使用串行收集器
  3. 如果是單核,并且沒有停頓時間的要求,串行或JVM自己選擇
  4. 如果允許停頓時間超過1秒,選擇并行或者JVM自己選
  5. 如果響應時間最重要,并且不能超過1秒,使用并發收集器
  6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,幾百G以上用ZGC

JDK 1.8預設使用 Parallel(年輕代和老年代都是)

JDK 1.9預設使用 G1

# 總結 以上就是今天要分享的内容,如有表述不清、錯誤還望海涵并指出。