您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關怎樣實現JVM垃圾回收,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
垃圾回收需要解決的第一個問題就是:如何判斷一個對象是否應該被回收,通常有這兩種算法:引用計數算法和可達性分析算法。
引用計數算法(Reference Counting Algorithm)
在對象中添加一個引用計數器,每增加一個對它的引用,計數就加1,每取消一個對它的引用,計數就減1,當計數器為0時,該對象就是可以回收的。引用計數算法原理簡單,判定高效,但需要額外占用內存空間,且無法解決循環引用。
可達性分析算法(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方法被完整執行,我們最好不要使用它。
由于方法區的垃圾回收性價比比較低,所以對方法區的回收并不是必須的,有些收集器也沒有實現方法區的回收。方法區的回收主要回收兩部分:廢棄的常量和不再使用的類型(類型卸載)。
廢棄的常量,如字符串常量池中的一個字符串,已經沒有任何字符串對象引用它,那么它就會被回收。
類型卸載,必須滿足三個條件:
類型卸載并不和對象回收一樣,滿足條件并不一定就肯定被回收,HotSopt可以通過-Xnoclassgc
參數控制。
該類的所以實例都已經被回收
加載該類的類加載器已經被回收
該類對應的Class對象沒有在任何地方被引用
當前大多數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收集器是一個出現非常早,實現簡單的新生代收集器,它是一個“單線程”(描述為串行可能更合適)收集器,額外消耗的資源非常小,在硬件資源受限的條件下,它是任然是首選的收集器,所以HotSpot VM的客戶端模式下默認的收集器就是Serial。Serial的缺點也非常的明顯,就是整個GC期間都會STW。
Serial old收集器是Serial的老年代版本,不同點是:Serial采用標記-復制算法,Serial old采用標記-整理算法。
相關參數:-XX:+UseSerialGC
PerNew收集器實質上是Serial收集器的多線程并行版本,也是一個新生代收集器,通常和CMS或Serial old(JDK9后不再支持)搭配使用。PerNew在單核甚至雙核處理器環境下,由于線程交互的開銷,效果沒有Serial好,但是隨著核心的增多效率提升還是非常明顯的。PerNew默認使用的線程數與處理器邏輯核心數量相同。
啟用參數:-XX:+UseParNewGC
,JDK 9 后此參數被取消
指定垃圾收集線程數:-XX:ParallelGCThreads
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是一款基于標記-整理算法的多線程并發收集器,它的出現就是為了和Scavenge搭配使用,它倆也是JDK 8的默認的收集器組合。啟動參數:-XX:+UseParallelOldGC
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(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使用了和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收集器是一款基于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可以直接從指針中看到其引用對象的三色標記狀態(Marked1、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,用于生產。
關于怎樣實現JVM垃圾回收就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。