走進(jìn)JVM-垃圾回收

??Java 會對內(nèi)存進(jìn)行自動分配與回收管理,使上層業(yè)務(wù)更加安全,方便地使用內(nèi)存實現(xiàn)程序邏輯。在不同的JVM 實現(xiàn)及不同的回收機(jī)制中,堆內(nèi)存的劃分方式是不-樣的。這里簡要介紹垃圾回收(Garbage Collection,GC)。垃圾回收的主要目的清除不再使用的對象,自動釋放內(nèi)存。
??GC 是如何判斷對象是否可以被回收的呢?為了判斷對象是否存活,JVM 引入了GC Roots。如果一個對象與 GC Roots 之間沒有直接或間接的引用關(guān)系,比如某個失去任何引用的對象,或者兩個互相環(huán)島狀循環(huán)引用的對象等,判決這些對象“死緩”,是可以被回收的。什么對象可以作為 GC Roots 呢? 比如: 類靜態(tài)屬性中引用的對象、常量引用的對象、虛擬機(jī)棧中引用的對象、本地方法棧中引用的對象等。
??有了判斷對象是否存活的標(biāo)準(zhǔn)后,再了解一下垃圾回收的相關(guān)算法。最基礎(chǔ)的為“標(biāo)記-清除算法”,該算法會從每個 GC Roots 出發(fā),依次標(biāo)記有引用關(guān)系的對象,最后將沒有被標(biāo)記的對象清除。但是這種算法會帶來大量的空間碎片,導(dǎo)致需要分配個較大連續(xù)空間時容易觸發(fā) FGC。為了解決這個問題,又提出了“標(biāo)記 - 整理算法”,該算法類似計算機(jī)的磁盤整理,首先會從 GC Roots 出發(fā)標(biāo)記存活的對象,然后將存活對象整理到內(nèi)存空間的一端,形成連續(xù)的已使用空間,最后把已使用空間之外的部分全部清理掉,這樣就不會產(chǎn)生空間碎片的問題?!癕ark-Copy”算法,為了能夠并行地標(biāo)記和整理將空間分為兩塊,每次只激活其中一塊,垃圾回收時只需把存活的對象復(fù)制到另一塊未激活空間上,將未激活空間標(biāo)記為已激活,將已激活空間標(biāo)記為未激活,然后清除原空間中的原對象。堆內(nèi)存空間分為較大的 Eden 和兩塊較小的Survivor,每次只使用 Eden 和 Survivor 區(qū)的一塊。這種情形下的“Mark-Copy”減少了內(nèi)存空間的浪費?!癕ark-Copy”現(xiàn)作為主流的YGC算法進(jìn)行新生代的垃圾回收。
??垃圾回收器(Garbage Collector)是實現(xiàn)垃圾回收算法并應(yīng)用在JVM 環(huán)境中的內(nèi)存管理模塊。當(dāng)前實現(xiàn)的垃圾回收器有數(shù)十種,本文章只介紹 Serial、CMS、G1三種。
??Serial 回收器是一個主要應(yīng)用于 YGC 的垃圾回收器,采用串行單線程的方式完成GC任務(wù),其中“Stop The World”簡稱STW,即垃圾回收的某個階段會暫停整個應(yīng)用程序的執(zhí)行。FGC 的時間相對較長,頻繁 FGC 會嚴(yán)重影響應(yīng)用程序的性能。主要流程如圖所示。


Servial回收流程

??CMS 回收器( Concurrent Mark Sweep Collector)是回收停頓時間比較短、目前比較常用的垃圾回收器。它通過初始標(biāo)記(Initial Mark)、并發(fā)標(biāo)記(Concurrent Mark )、重新標(biāo)記 ( Remark )、并發(fā)清除(Concurrent Sweep)四個步完成垃圾收工作。第1、3步的初始標(biāo)記和重新標(biāo)記階段依然會引發(fā) STW,而第 2、4步的并發(fā)標(biāo)記和并發(fā)清除兩個階段可以和應(yīng)用程序并發(fā)執(zhí)行,也是比較耗時的操作,但并不影響應(yīng)用程序的正常執(zhí)行。由于 CMS 采用的是“標(biāo)記 - 清除算法”,因此產(chǎn)生大的空間碎片。為了解決這個問題,CMS 可以通過配置-XX:+UseCMSCompactAtFulCollection=n參數(shù),強(qiáng)制JVM 在FGC 完成后對老年代進(jìn)行壓縮,執(zhí)行一次空間碎片整理但是空間碎片整理階段也會引發(fā) STW。為了減少 STW 次數(shù),CMS 還可以通過配置XX:+CMSFullGCsBeforeCompaction=n 參數(shù),在執(zhí)行了次 FGC后,JVM再在老年代執(zhí)行空間碎片整理。
??Hotspot 在JDK7中推出了新一代G1( Garbage-First Garbage Collector )垃圾回通過-XX:+UseG1GC 參數(shù)啟用。和 CMS 相比,G1 具備壓縮功能,能免碎片問題G1的暫停時間更加可控。性能總體還是非常不錯的,簡要結(jié)構(gòu)如圖所示。


G1回收模型內(nèi)存布局

??G1將Java 堆空間分割成了若干相同大小的區(qū)域,即region,包括Eden、Survivor、Old、Humongous 四種類型。其中,Humongous 是特殊的 Old 類型,專放置大型對象。這樣的劃分方式意味著不需要一個連續(xù)的內(nèi)存空間管理對象。G1將空間分為多個區(qū)域,優(yōu)先回收垃圾最多的區(qū)域。G1采用的是“Mark-Copy”,有非好的空間整合能力,不會產(chǎn)生大量的空間碎片。G1的一大優(yōu)勢在于可預(yù)測的停頓時間能夠盡可能快地在指定時間內(nèi)完成垃圾回收任務(wù)。在JDK11 中,已經(jīng)將 G1設(shè)為默認(rèn)垃圾回收器,通過jstat命令可以查看垃圾回收情況,如圖所示,在YGC時SO/S1并不會交換。
G1內(nèi)存回收情況

??S0/S1的功能由G1中的 Survivor region 來承載。通過GC 日志可以觀察到完整的垃圾回收過程如下,其中就有 Survivor regions 的區(qū)域從0個到1個。
[0.530s]linfo llge,start ] GC(O) Pause Initial Mark (G1 Humongous Allocation)
[0.530s][info][gc,task]GC(0) Using 4 workers of 4 for evacuation
[0.535s][info lgc,heap]GC(0) Eden regions:2->0(152)
[0.535s][info ][gc,heap]GC(0) Survivor regions:0->1(2)
[0.535s][info ][gc,heap]GC(0) Old regions:0->0
[0.535s][info ][gc,heap]GC(0) Humongous regions: 115->39
[0.535s][info llgc,metaspace ]GC(0) Metaspace: 6001K->6001K(1056768K)

??紅色標(biāo)識的為 G1中的四種region,都處于Heap中。G1執(zhí)行時使用4個worker 并發(fā)執(zhí)行,在初始標(biāo)記時,還是會觸發(fā)STW,如第一步所示的 Pause。G1 的Concurrent Marking 分為五個主要步聚
??第一步,Initial Mark,其實就是 YoungGC。該階段會引起STW,它會標(biāo)記GC Roots 直接可達(dá)的存活對象。GC 日志如下:[GC pause(G1 Evacuation Pause)(young)(initial-mark),0.0008405 secs]。
??第二步,Root Region Scan,即根區(qū)域掃描。該階段不會引起STW,它會并發(fā)地從上一階段標(biāo)記的存活區(qū)域中掃描被引用的老年代對象。GC日志如下:[GC concurrent-root-region-scan-start][GC concurrent-root-region-scan-end,0.0000050 secs]。
??第三步,Concurrent Mark,即并發(fā)標(biāo)記。該階段從堆中標(biāo)記存活的對象,與CMS類似。GC日志如下[GC concurrent-mark-startGC concurrent-mark-end,0.0169973secs]。
??第四步,Remark,即重新標(biāo)記。該階段會引起STW,與CMS 類似。它會完成最終的標(biāo)記處理。GC日志如下:[GC remark[Finalize Marking,0.0001883 secs][GC-ref-proc,0.0000471 secs] [Unloading,0.0008435 secs],0.0055849 secs][Times: user=0.03sys=0.01,real=0.00 secs] 。
??第五步,Cleanup,主要為接下來的 Mixed GC 做準(zhǔn)備。該階段會統(tǒng)計所有堆區(qū)域中的存活對象,并將待回收區(qū)域按回收價值排序,優(yōu)先回收垃圾最多的區(qū)域GC日志如下:[GC cleanup 2308M->2308M(3929M),0.0030328 secs][Times: user-0.01 sys=0.00,real=0.00 secs]。
??在JDK11 版本中,引入試驗性質(zhì)的新 GC 算法 ZGC,它是一個可伸縮的低延垃圾收集器,宣稱暫停時間不超過10毫秒。ZGC會因為GC Root增大而增加暫停時間比如很多 Thread。Thread 的堆棧很深,但是與堆大小以及 Live Data Size 無關(guān)。ZG處理堆范圍從幾百 MB 到幾TB。ZGC 的目標(biāo)是吞吐量降低不超過 15%,與G1一樣在ZGC中將堆內(nèi)存分成大量的內(nèi)存區(qū)域,即 ZPage,區(qū)別是 ZGC中的區(qū)域大小是不相同的,有小型、中型和大型之分。在小型 page 中分配小對象,最大為 256KB;在中型 page 中分配中等大小的對象,最大為4MB;在大型 page中分配大于4MB 的對象ZGC 包括十個階段,最主要的兩個階段是 mark 和relocate,GC 不斷從標(biāo)記階段開始循環(huán),遞歸所有可達(dá)對象,標(biāo)記結(jié)束時可以知道哪些對象可以被回收。ZGC 將記結(jié)果存儲在每個page的 live bitmap 中。在標(biāo)記過程中,應(yīng)用線程中的load barrie將暫時未標(biāo)記的引用對象壓入緩沖區(qū)。一旦緩沖區(qū)滿,GC 線程會遞歸遍歷此緩沖區(qū)中所有可達(dá)對象。標(biāo)記結(jié)束后,ZGC 需要遷移relocate 集合中所有的對象。relocate集合是一組 page 集合,根據(jù)某些標(biāo)準(zhǔn),系統(tǒng)決定是否需要遷移它們。ZGC為每個relocate集合的頁面分配了forwarding table。它是一個哈希映射(如果對象已經(jīng)被遷)它存儲一個對象被移動后的新地址。為了實現(xiàn) ZGC 的目標(biāo),增加了兩種方式:著色指針和讀屏障。前者使用簡潔的多重映射技巧可以處理更多的內(nèi)存。所謂試驗性質(zhì)是指該策略在JDK11中只會支持 Linux 系統(tǒng),其他平臺暫不支持,并且在生產(chǎn)環(huán)境中還需要考慮如下問題:第一,如何支持 class unloading,這是一個功能性缺失;第二,compressed ref 和ZGC 是沖突的,開ZGC,一定不能有 compressed ref; 第三,解決single generation 問題,因為應(yīng)用的分配速率過高的話,GC 有可能跟不上,這可能是潛在問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容