Grabage Collection
參考:https://www.programmersought.com/article/54974474710/java
垃圾回收算法
引用計數和可達性性分析
垃圾回收須要解決的第一個問題就是:如何判斷一個對象是否應該被回收,一般有這兩種算法:引用計數算法和可達性分析算法。算法
-
引用計數算法(Reference Counting Algorithm)shell
在對象中添加一個引用計數器,每增長一個對它的引用,計數就加1,每取消一個對它的引用,計數就減1,當計數器爲0時,該對象就是能夠回收的。引用計數算法原理簡單,斷定高效,但須要額外佔用內存空間,且沒法解決循環引用。windows
-
可達性分析算法(Reachability Analysis Algorithm)數組
經過一系列稱爲「 GC Roots」的根對象做爲起始節點集,從這些節點開始向下搜索,搜索所通過的路徑稱爲「引用鏈」(Reference Chain),若是對象到GC Roots沒有任何引用鏈相連,則意味着該對象不可訪問,即該對象沒法再使用,則能夠回收。JVM使用的就是這個算法。緩存
Java中固定可做爲GC Roots的對象:安全
- 虛擬機棧棧幀中本地變量表中引用的對象,如各個線程被調用方法使用的參數、變量等
- 方法區中類靜態屬性引用的對象,如Java類變量
- 方法去中常量引用的對象,如字符串常量池的引用
- 本地方法棧JNI引用的對象
- JVM內部的引用,如基本數據類型對應的Class對象、常駐的異常對象、系統加載器等
- 全部被同步鎖(syschronized)持有的對象
- 反應JVM內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等
-
finalize方法 對象在可達性分析中標記爲不可達後,並不必定就會被回收,能夠在finalize方法中拯救一下本身。若是對象覆蓋了finalize方法,而且finalize方法從未被執行過,那麼該對象會被放置到F-Queue隊列中,稍後會有單獨的JVM線程執行隊列中對象的finalize方法,但並不承諾必定會等待它運行結束。收集器會對F-Queue中的對象進行二次的標記,若是在finalize方法中從新與引用鏈上的對象創建了關聯,那麼二次標記將會把它移除回收的集合。特別注意的是,任何對象的finalize方法只會被執行一次,下一次回收時將不會被執行。因爲並不能保證finalize方法被完整執行,咱們最好不要使用它。數據結構
方法區的回收
因爲方法區的垃圾回收性價比比較低,因此對方法區的回收並非必須的,有些收集器也沒有實現方法區的回收。方法區的回收主要回收兩部分:廢棄的常量和再也不使用的類型(類型卸載)。多線程
-
廢棄的常量,如字符串常量池中的一個字符串,已經沒有任何字符串對象引用它,那麼它就會被回收。架構
-
類型卸載,必須知足三個條件:
- 該類的因此實例都已經被回收
- 加載該類的類加載器已經被回收
- 該類對應的Class對象沒有在任何地方被引用
類型卸載並不和對象回收同樣,知足條件並不必定就確定被回收,HotSopt能夠經過
-Xnoclassgc
參數控制。
分代收集理論
當前大多數JVM垃圾收集器都遵循分代收集(Generational Collection)理論,它基於下面三個假說:
- 弱分代假說:絕大多數對象都是朝生夕滅的
- 強分代假說:熬過越屢次GC過程的對象越難以被回收
- 跨代引用假說:跨代引用相對同代引用僅佔極少數
基於前兩個假說,收集器將Java堆分爲了新生代和老年代。新生代中絕大多數對象都是朝生夕滅的,每次GC均可以回收均可以清理大量的內存空間,而且能夠更加頻繁的執行回收。老年代中的對象難已被回收,GC結果遠低於新時代,回收頻率更低。
跨代引用較少的緣由:存在引用關係的對象是應該傾向於同時生存或消亡的。若某個新生代對象存在跨代引用,因爲老年代對象難以消亡,隨着屢次GC,新生代對象就會晉升到老年代,從而消除了這種跨代引用。由於跨代引用上應該不多,因此在Minor GC時,不能爲了解決少數的跨代引用就把整個老年代對象都加入到GC Roots中,那樣太影響效率了。
跨代引用的解決方法
在收集區中創建一個全局的數據集——記憶集,記錄從非收集區指向收集區的的全部引用。記憶集的實現有多種方法,大多數收集器都採用卡表的方式。舉例來講,在新生代中建立一個卡表,而後把老年代劃分紅若干塊,在卡表中記錄老年代那一塊內存存在跨代引用,Minor GC時,只須要把存在跨代引用的那塊內存對象加入到GC Roots中。
HotSopt中卡表的實現是一個byte數組,數組元素的值爲0和1兩種,不用bit數組的緣由是現代計算機硬件都是最小按字節尋址的,用bit反而須要多條指令。byte數組中每個元素對應一個大小爲512 byte的內存塊,每一個對象的內存地址除以512就是其在數組中的位置,這個內存塊叫卡頁,一個卡頁一般不止包含一個對象,當卡頁中有一個對象的字段存在跨代引用時,那麼卡表中相應的元素的值就是1,GC時只要把卡表中值爲1的元素對應的卡頁中的對象加入到GC Roots中就能夠了。
至於爲何要用卡表這樣粗曠的粒度,是由於能夠節約存儲空間和維護成本。
何時改變卡表中的值?有其餘分代區域的對象引用了本區域的對象時,就須要改變卡表中的值。
如何改變卡表中的值——寫屏障。寫屏障和AOP的思想很像,使用寫屏障,JVM會爲全部的賦值操做生成相應的指令,來維護卡表中的值。
標記-清除算法
收集器首先標記出須要被回收的對象,標記完成後統一回收全部被標記過的對象,固然也能夠反過來標記不須要被回收的對象,而後清理未被標記的對象。它有兩個明顯的缺陷:
- 執行效率不穩定,若是堆中包含大量對象且大部分須要被回收,則必須進行大量的標記和清理動做,執行效率是隨着對象增多而下降的。
- 產生大量不連續的內存碎片。
標記-複製算法
將內存劃分爲兩個大小相等的區域,每次只使用其中一塊,這塊區域用完時,將存活的對象複製到另外一塊中,而後將已滿這塊內存空間一次清理掉。若是大多數對象都是可回收的,那麼複製少許的對象開銷就很小,因爲只須要一次清理,所以執行效率相比於標記清除是很高且穩定的,並且也不會存在內存碎片。代價就是每次可用的內存空間只有一半。
實際中,大多數收集器都採用這種算法回收新生代,將新生代分爲一個Eden區和兩個Survivor區,默認比例爲8:1:1,每次只使用Eden區和一個Survivor區,即新生代的可用區域只有實際區域的90%,Minor GC時將存活對象複製到另外一Survivor區,而後清理掉Eden和已經用過的Survivor區。若是GC時存活下來的對象須要的空間超過了Survivor的大小,則會直接放入老年代中,這就是用老年代內存進行分配擔保。
標記-整理算法
很明顯標記複製算法是不太適合老年代的,老年代GC一般使用標記整理,即先標記出可被回收的對象,而後讓存活的對象向內存空間的一側移動,而後直接清理掉邊界之外的內存。
和標記-清除相比最大的不一樣就是須要移動活着的對象,移動對象能夠規避內存碎片,且須要更新全部引用這些對象的地方,但分配內存時更加簡單;不移動對象,回收內存簡單,而分配內存時會由於內存碎片變得複雜。因此不一樣的收集器會根據本身的特性來選擇適合本身的算法,如Parallel Old關注吞吐量則使用了標記複製,CMS關注延遲則使用了標記清除。
根節點枚舉
迄今爲止,全部的收集器在枚舉GC Roots時都是須要凍結全部用戶線程的,即STW(Stop The World),所以如何高效的找到全部的根節點對象是必須解決的。HotSpot採用了準確式GC以提高GC roots的枚舉速度。所謂準確式GC,就是讓JVM知道內存中某位置數據的類型什麼。好比當前內存位置中的數據到底是一個整型變量仍是一個引用類型。這樣JVM能夠很快肯定全部引用類型的位置,從而更有針對性的進行GC roots枚舉。
HotSpot是利用OopMap來實現準確式GC的。當類加載完成後,HotSpot 就將對象內存佈局之中什麼偏移量上數值是一個什麼樣的類型的數據這些信息存放到 OopMap 中;在 HotSpot 的 JIT 編譯過程當中,一樣會插入相關指令來標明哪些位置存放的是對象引用等,這樣在 GC 發生時,HotSpot 就能夠直接掃描 OopMap 來獲取對象引用的存儲位置,從而進行 GC Roots 枚舉。(?仍是不明白爲何須要OopMap)
安全點和安全區域
因爲致使引用關係變化即改變OopMap內容的指令很是多,若是每條指令都生成對應的OopMap,那將耗費大量額外空間,因此HotSpot只在特定位置記錄這些信息,這個位置就是安全點。安全點的存在就要求GC時,用戶線程必須執行到安全點後才能暫停,而不能在指令流的任意位置暫停。當收集器須要暫停用戶線程時,並不直接對線程操做,而是設置一個標誌位,各個線程執行過程當中會不停的主動輪詢這個標誌,若是標誌爲真則在最近的安全點上主動中斷掛起。
安全點有一個問題不能解決,那就是用戶線程若是處於sleep或blocked狀態時,確定是不能響應中斷本身走到安全點的,因而就須要安全區域來解決。安全區域就是指能保證在某一代碼片斷中,引用關係不會發生變化,所以在這個區域中任何地方開始GC都是安全的。當用戶線程執行到安全區域的代碼時,就會標識本身進入到安全區域,那這段時間JVM要發起GC就沒必要管這些已在安全區域的線程了。當線程要離開安全區域時,它就必須檢查JVM是否已經完成了根節點的枚舉,若是沒有則必須一直等待。
併發的可達性分析
根節點枚舉完成後,須要從GC Roots開始遍歷整個對象圖,這個過程是很是耗時的,所以一些收集器爲了減小GC的STW時間,讓遍歷對象圖的過程和用戶線程併發執行,這樣就極大減小了STW時間,可是若是用戶線程在此期間修改引用關係,就會形成標記錯誤,以下面這個圖:
上圖中對象D就被錯誤的回收掉。致使一個白色對象被錯誤回收掉須要同時知足兩個條件:
- 賦值器插入了一條或多條從黑色對象到白色對象的新引用
- 賦值器刪除了所有灰色對象到該白色對象的直接或間接引用
解決的兩種辦法:
-
增量更新:破壞條件1,當黑色對象插入了新的指向白色對象的引用時,將新插入的引用記錄下來,等併發掃描結束後,再將這些記錄中的黑色對象看成Roots從新掃描一次。
-
原始快照:破環條件2,當灰色對象刪除指向白色對象的引用時,將要刪除的引用記錄下來,當併發掃描結束後,以灰色對象爲Roots,從新掃描一次。注意了,第二次掃描是掃描的原始數據(因此叫原始快照),第二次掃描發現了C到D的引用,所以D會被判爲存活。這樣有一個問題,那就是若是沒有新增A到D的引用,此時D會被誤判爲存活,但這是不要緊的,由於把應該回收的對象被判存活遠比把應該存活的對象判爲回收問題小的的,不過是等待下一次回收而已。
在HotSpot中,CMS使用增量更新作併發標記,G1和Shenandoah則使用了原始快照。
垃圾收集器
Serial/Serial old
Serial收集器是一個出現很是早,實現簡單的新生代收集器,它是一個「單線程」(描述爲串行可能更合適)收集器,額外消耗的資源很是小,在硬件資源受限的條件下,它是任然是首選的收集器,因此HotSpot VM的客戶端模式下默認的收集器就是Serial。Serial的缺點也很是的明顯,就是整個GC期間都會STW。
Serial old收集器是Serial的老年代版本,不一樣點是:Serial採用標記-複製算法,Serial old採用標記-整理算法。
相關參數:-XX:+UseSerialGC
PerNew
PerNew收集器實質上是Serial收集器的多線程並行版本,也是一個新生代收集器,一般和CMS或Serial old(JDK9後再也不支持)搭配使用。PerNew在單核甚至雙核處理器環境下,因爲線程交互的開銷,效果沒有Serial好,可是隨着核心的增多效率提高仍是很是明顯的。PerNew默認使用的線程數與處理器邏輯核心數量相同。
啓用參數:-XX:+UseParNewGC
,JDK 9 後此參數被取消
指定垃圾收集線程數:-XX:ParallelGCThreads
Parallel Scavenge
Parallel Scavenge也是一個面向新生代、採用標記-複製、並行收集的收集器。Scaveng與PerNew很是的類似,GC過程也是同樣,但它的主要關注點是系統的吞吐量。
-
能夠控制系統的吞吐量
# 每次GC的最大停頓時間,參數值爲大於0的毫秒數 -XX:MaxGCPauseMillis # 吞吐量,參數值爲大於0小於100的整數,默認值爲99,即GC的時間最多佔系統容許總時間的1/(1+99)=1% -XX:GCTimeRatio
-
Scavenge可以根據系統運行狀況,動態調整Eden區和Survivor區的比例、對象晉升老年代年齡等參數以提供合適的停頓時間或最大的吞吐量。開啓這個功能的參數爲
-XX:+UseAdaptiveSizePolicy
Parallel Scavenge是JDK 8默認的年輕代垃圾收集器,啓動參數爲-XX:+UseParallelGC
。
Parallel Old
Parallel old是一款基於標記-整理算法的多線程併發收集器,它的出現就是爲了和Scavenge搭配使用,它倆也是JDK 8的默認的收集器組合。啓動參數:-XX:+UseParallelOldGC
CMS
CMS(Concurrent Mark Sweep)是一款基於標記-清除算法的老年代收集器,它是爲了減小GC停頓時間而出現的。前面的幾款收集器都有一個致命弱點:GC的全過程都須要STW,CMS雖然沒能完全解決這個問題,但卻大大縮短了STW的時間。它把GC過程分紅了下面幾個步驟:
-
初始標記 (initial mark)
須要STW,僅標記GC Roots能直接關聯的對象,這個過程是很是快的。
-
併發標記(concurrent mark)
不須要STW,從上一步獲得的對象開始遍歷對象圖,這個過程耗時較長但與用戶線程併發運行的。
-
從新標記(remark)
須要STW,修正併發標記期間因用戶線程運行而致使標記變更的那一部分對象的標記記錄,耗時較短。
-
併發清除(concurrent sweep)
不須要STW,併發清除被標記死亡的對象,因爲是直接清除不須要整理,因此也是與用戶線程併發運行的。
啓用參數:-XX:+UseConcMarkSweepGC
CMS的缺點:
- 因爲採用標記-清除算法,所以內存碎片是不可避免的,當碎片過多沒法給大對象分配空間時就會觸發Full GC。
- 沒法處理浮動垃圾。在併發標記和併發整理期間新產生的垃圾,在本次GC沒法清理掉,只能留到下一次GC。一樣因爲GC時,用戶線程任然在運行,因此不能等到老年代空間所有被佔滿了纔開始GC,須要留一些空間給GC期間的用戶線程使用,所以還剩多少空間的時候開始GC(
-XX:CMSInitialingOccupancyFracion
),這個很關鍵,若是GC期間預留的內存沒法知足對象的分配使用,出現併發失敗,這時JVM就會啓用Serial old來從新進行老年代的垃圾收集。 - 並行處理期間雖然不會凍結用戶線程,但收集器自身會佔用一部分系統資源,致使用戶線程的吞吐量降低。CMS默認使用的線程數是$(處理器核心數+3)/4$。
G1
在G1(Garbage First)以前,垃圾收集器的目標範圍很明確:新生代(Minor GC),老年代(Major GC)以及整堆(Full GC)。G1則再也不這樣,它能夠面向堆內存任何部分來組成回收集進行回收,衡量標準再也不是它屬於哪一個分代,而是那塊內存中垃圾數量最多,回收收益最高,這就是G1的Mixed GC模式。從G1開始,大部分新的收集器都再也不追求一次性把垃圾清理乾淨,而是追求可以應付應用的內存分配速率,只要回收速度可以跟上新對象內存分配的速度,那就是完美的。
G1再也不堅持固定大小或數量的分代區域劃分,而是把連續的堆劃分爲多個大小相等的獨立區域(Region),每一個Region均可以根據須要扮演Eden、Survivor或Old空間(G1依然是按照分代收集的)。每一個Region的大小能夠經過參數-XX:G1HeapRegionSize
設定,取值範圍爲1MB-32MB,且應爲2的N次冪。Region中有一類專門用來存放大對象的區域——Humongous Region,G1認爲超過Region容量一半的對象就是大對象,而那些超過Region容量的大對象,將會被存放在N個連續的Humongous Region中。
G1把Region做爲單次回收的最小單元,跟蹤每一個Region回收所能得到的空間大小及須要的時間,而後在後臺維護一個優先級列表,每次根據用戶設定的容許停頓時間(-XX:MaxGCPauseMillis
,默認200ms),優先回收那些價值最大的Region,這樣就保證了在有限的時間及可能高的回收效率。這樣的內存佈局使得G1從總體上看是標記-整理,從兩個Region看又是標記-複製,避免了CMS的採用標記-清除所帶來的內存碎片問題。
爲了解決「跨代」引用問題,G1讓每一個Region都有本身的記憶集,其記錄了哪個Region引用了本Region的對象。記憶集的實現是一個hash表,key是別的Region的起始地址,value是一個集合,裏面存儲的元素是卡表的索引號。所以G1的記憶集比CMS的卡表維護起來更加複雜,且佔用空間更大,記憶集耗費的空間大體上佔堆空間的10%到20%。
G1在併發標記階段採用原始快照算法,而且爲每一個Region加入了兩個TAMS(Top at Mark Start)指針,把Region中分出一部分空間用於併發過程當中新對象下的分配,這部分空間的對象默認存活的,不會歸入當前GC的回收範圍。
G1收集器的運做的大體分爲四個步驟:
-
初始標記
須要STW,標記GC Roots能直接關聯的對象,修改TAMS指針的值。
-
併發標記
不須要STW,從上一步獲得的對象開始遍歷對象圖。
-
最終標記
須要STW,處理併發標記期間有引用變更的對象
-
篩選回收
須要STW,更新Region的統計數據,對各個Region的回收價值和成本排序,根據用戶指望的停頓時間制定回收計劃,自由選擇多個Region構成回收集,把決定回收的Region中存活的對象複製到空的Region中,而後清理掉整個Region空間。
G1是JDK11默認的收集器。啓用參數:-XX:+UseG1GC
G1的缺點:
- 因爲每一個Region都有記憶集且結構更復雜,致使內存佔用比CMS要高。
- 除了用寫後屏障維護更復制的記憶集外,還需使用寫前屏障來跟蹤併發標記階段引用的變化,會爲用戶程序帶來額外的負擔。
Shenandoah
Shenandoah使用了和G1同樣的Region內存佈局,默認也是優先處理回收價值大的Region,其與G1的最大不一樣是:
-
默認不使用分代收集
-
回收階段能夠與用戶線程並行
-
使用鏈接矩陣(Connection Matrix)的全局數據結構來記錄跨Region的引用關係。
如上圖,Region5的對象引用Region3的對象,就在3行5列打個標記;Region3引用了Region1的對象,就在1行3列上打個標記(圖上位置是錯誤的)。
Shenandoah收集器的工做過程大體分爲九個過程:
-
初始標記
須要STW,標記GC Roots能直接關聯的對象。
-
併發標記
不須要STW,從上一步獲得的對象開始遍歷對象圖。
-
最終標記
須要STW,處理併發標記期間有引用變更的對象,統計出回收價值最高的Region構成回收集。
-
併發清理
不須要STW,清理那些沒有一個存活對象的的Region。
-
併發回收
不須要STW,將存活對象複製到未使用的Region中。
Shenandoah是如何解決複製對象時,用戶線程訪問被複制的對象問題,由於這個時候其餘對象持有的引用並未更新,若是這個時候讀寫該對象,會形成數據不一致。辦法就是在原有的對象佈局結構的最前面統一增長一個新的引用字段——即轉發指針,在正常不處於併發移動的狀況下,轉發指針指向對象本身,移動時,修改轉發指針的值爲新地址,這樣即可以將對該對象的全部引用轉發到新副本上。爲了併發安全,對轉發指針的修改採用了CAS。
-
初始引用更新
須要STW,創建一個線程集合點,確保全部併發回收線程都已經完成對象的移動。
-
併發引用更新
不須要STW,把堆中指向舊對象的引用修正到複製後的新地址。
-
最終引用更新
須要STW,修正GC Roots的引用。
-
併發清理
不須要STW,併發清理Region的內存空間。
從上面的九個步驟就能夠看出,Shenandoah的停頓時間是很是短的,全部的耗時步驟均可以與用戶線程並行,所以Shenandoah是一款低延遲的收集器,實際運行中GC的平均停頓時間不會超過50ms。
啓用參數:-XX:UseShenandoahGC
,openjdk12加入,目前任處於實驗階段,預計JDK15可用於生產。
ZGC
ZGC收集器是一款基於Region內存佈局的,不設分代,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記-整理算法的,以低延遲爲首要目標的一款垃圾收集器。參考
Region內存佈局
ZGC也採用了基於Region的內存佈局,與G1不一樣的是,ZGC的Region具備動態性——動態建立和銷燬及動態的區域容量大小,大體分爲三類:
- 小型Regoin(Small Region):容量固定2MB,用於放置小於256KB的對象。
- 中性Region(Medium Region):容量固定爲32MB,放置大於等於256KB小於4MB的對象。
- 大型Region(Large Region):容量不固定,可動態變化,但必須爲2MB的整數倍,用於放置4MB及以上的對象。每一個Large Region中只會存放一個大對象,所以Large Region的容量最小可能只有4MB。
染色指針
一個對象只有它的引用關係能決定它存活於否,對象自身的因此屬性都不能影響它存活的斷定結果,所以把標記信息直接記錄在對象指針上是最最直接的,Serial收集器記錄在對象頭中,G1用了一個單獨的位圖存儲。染色指針(Colored Pointer)就是一種直接將少許額外信息存儲在指針上的技術。在64位系統中,理論能夠訪問的內存高達16EB(64位地址),但實際在AMD64架構中只支持到52位(4PB)的地址總線和48位(256TB)的虛擬地址空間,在操做系統層面,64位Linux則只支持47位(128TB)的進程虛擬地址空間和46位(64TB)的物理地址空間,Windows系統則更少。所以,ZGC的就將Liunx中46位指針中的高4位提取出來存儲四個標誌信息,經過這些標誌位,JVM能夠直接從指針中看到其引用對象的三色標記狀態(Marked一、Marked0)、是否進入了從分配集(即被移動過,Remapped),是否只能經過finalize方法才能被訪問到(Finalizable)。這也致使了ZGC的可以管理的內存不能夠超過4TB(42位,JDK13增長到了16TB),不能支持32位平臺,而且不能使用壓縮指針。
染色指針帶來的優點也是很是明顯的,以下:
- 染色指針使得Region中存活的對象被移走後,該Region就能夠當即釋放或重用,而沒必要像Shenandoah那樣須要等待堆中指向該Region的引用都修正後才能清理。所以理論上來講,只要還有一個空閒的Region,ZGC都能完成收集。
- 染色指針能夠大幅減小在垃圾收集過程當中內存屏障的使用數量,由於引用變更信息已經維護在了指針中,就能夠省去之前那些經過寫屏障來記錄的操做了。
- 染色指針能夠做爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便往後進一步提升性能。好比想辦法用未使用的高18位來作一些事情
多重映射
要了解多重映射的工做原理,咱們須要簡要解釋虛擬內存和物理內存之間的區別。 物理內存是系統可用的實際內存,一般是安裝的DRAM芯片的容量。 虛擬內存是抽象的,這意味着應用程序對(一般是隔離的)物理內存有本身的視圖。 操做系統負責維護虛擬內存和物理內存範圍之間的映射,它經過使用頁表和處理器的內存管理單元(MMU)和轉換查找緩衝器(TLB)來實現這一點,後者轉換應用程序請求的地址。
Linux/X84-64平臺上ZGC使用了內存多重映射(Multi-Mapping)將多個不一樣虛擬機內存地址映射到同一物理地址上,這是一種多對一映射,意味着ZGC在虛擬內存中看到的地址空間要比實際堆內存空間更大。把染色指針中的標誌位看做是地址的分段符,那隻要將這些不一樣的地址段都映射到同一物理內存空間,通過多重映射轉換後,就可使用染色指針正常進行尋址了,以下圖:
工做過程
ZGC的運做過程大體可劃分如下階段:
-
初始標記
須要STW,標記GC Roots能直接關聯的對象。ZGC每次回收都會掃描全部的Region,用範圍更大的掃描成原本省去G1中記憶集的維護成本。
-
併發標記
不須要STW,從上一步獲得的對象開始遍歷對象圖,更新染色指針的Marked0、Marked1標誌位。
-
最終標記
須要STW,處理併發標記期間有引用變更的對象。
-
併發預備重分配(Concurrent Prepare for Relocate)
不須要STW,根據特定查詢條件統計出本次收集過程當中要清理那些Region,將這些Region組成重分配集。因爲是掃描了全部的Region,ZGC的重分配集只決定了裏面存活的對象會被複制到其餘Region,分配集中Region被釋放,因此標記是針對全堆的,回收卻不是。
-
初始重分配(Relocate Start)
須要STW,將重分配集中的GC Roots直接關聯的對象複製到新的Region中。
-
併發重分配(Concurrent Relocate)
不須要STW,將重分配集中的存活對象複製到新的Region中,併爲分配集中的每一個Region維護一個轉發表(不是在Region中,在單獨的區域),記錄從舊對象到新對象的轉發關係。在此期間用戶線程訪問了位於重分配集中的對象,此次訪問將會被預置的內存屏障截獲,而後根據Region的轉發表將訪問轉發到新對象上,並同時修正更新該引用的值,使其指向新對象——ZGC把這種行爲稱爲指針的自愈能力。這樣作的好處是隻有第一次訪問舊對象會陷入轉發,而不像Shenandoah的轉發指針那樣每次都存在轉發開銷。
-
併發重映射(Concurrent Remap)
不須要STW,修正整個堆中指向重分配集中就對象的引用,完成後釋放掉轉發表。因爲自愈能力的存在,這個步驟並不須要立刻完成,所以ZGC把該步驟合併到了下一次回收的併發階段裏去完成,省了一次遍歷對象圖的開銷。
開啓方法:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
,ZGC在JDK11的Linux版正式以實驗性質加入,mac和windows則須要JDK14,預計在JDK15中正式Release,用於生產。