一文了解 Java 8 - 18,垃圾回收的十次進(jìn)化

經(jīng)歷了數(shù)千次改進(jìn),Java的垃圾回收在吞吐量、延遲和內(nèi)存大小方面有了巨大的進(jìn)步。
2014年3月JDK 8發(fā)布,自那以來JDK又連續(xù)發(fā)布了許多版本,直到今日的JDK 18是Java的第十個(gè)版本。借此機(jī)會,我們來回顧一下HotSpot JVM的垃圾回收器的發(fā)展全過程。
關(guān)于垃圾回收、度量和取舍
HotSpot JVM中負(fù)責(zé)管理應(yīng)用程序堆的組件叫做“垃圾回收器”(Garbage Collector,即GC)。GC負(fù)責(zé)管理應(yīng)用程序堆對象的整個(gè)生命周期,從應(yīng)用程序分配內(nèi)存到內(nèi)存被回收,都由GC負(fù)責(zé)。
從高層來看,JVM垃圾回收算法的最基本功能如下:
當(dāng)應(yīng)用程序請求分配內(nèi)存時(shí),GC負(fù)責(zé)提供內(nèi)存。提供內(nèi)存的過程應(yīng)盡可能快。
GC檢測應(yīng)用程序不再使用的內(nèi)存。這個(gè)操作也應(yīng)當(dāng)十分高效,不應(yīng)消耗太多時(shí)間。這種不再使用的內(nèi)存稱為“垃圾”。
GC將同一塊內(nèi)存再次提供給應(yīng)用程序,最好是“實(shí)時(shí)”,也就是要快。
好的垃圾回收算法還有更多的需求,但這三條是最基本的,也足以支撐本文的討論了。
滿足這些需求有很多方法,但很不幸,我們并沒有一蹴而就的方法,也沒有能一次性解決所有需求的方法。因此,JDK提供了多種垃圾回收算法以供選擇,每種算法適用于不同的場景。這些算法的實(shí)現(xiàn)基本上可以根據(jù)吞吐量、延遲和內(nèi)存大小這三個(gè)性能度量,以及對應(yīng)用程序的影響進(jìn)行歸類。
吞吐量指的是單位時(shí)間內(nèi)能夠完成的工作量。在此語境下,垃圾回收算法的優(yōu)劣取決于能在單位時(shí)間內(nèi)完成的回收工作量,這些算法可以讓Java應(yīng)用程序?qū)崿F(xiàn)更高的吞吐量。
延遲指的是單次操作所需時(shí)間。垃圾回收算法需要盡可能減小延遲。在垃圾回收的語境下,關(guān)鍵點(diǎn)就是垃圾回收期是否會導(dǎo)致暫停、暫停的范圍,以及暫停的時(shí)長。
在垃圾回收的語境下,內(nèi)存大小指的是為了讓垃圾回收期正常工作,需要在正常的應(yīng)用程序堆內(nèi)存之外,再額外占用多少內(nèi)存。如果GC(或更一般地,JVM)需要的內(nèi)存很少,就可以給應(yīng)用程序堆留出更多內(nèi)存。
這三個(gè)度量是互相關(guān)聯(lián)的:高吞吐量的垃圾回收器可能會嚴(yán)重影響延遲(但對應(yīng)用程序的影響最?。?。為了降低內(nèi)存消耗,我們需要采用在其他度量方面不是那么出色的算法。延遲較低的回收期需要并行進(jìn)行更多工作,或以更小的單位進(jìn)行工作,這就會消耗更多處理器資源。
這些關(guān)系通常可以畫成一個(gè)三角形,如圖1所示。每個(gè)垃圾回收算法占據(jù)三角形的一個(gè)角。

圖1. GC性能度量三角
提高GC在某方面的表現(xiàn),通常會導(dǎo)致其他方面的表現(xiàn)降低。
JDK 18中的OpenJDK GC
OpenJDK提供了五種GC,分別專注于不同的性能度量。表1列出了GC的名稱、專注領(lǐng)域,以及實(shí)現(xiàn)特定特性所使用的核心概念。
表1. OpenJDK的五種GC

Parallel GC是JDK 8以及更早版本的默認(rèn)回收期。它專注于吞吐量,盡快完成工作,而很少考慮延遲(暫停)。
Parallel GC會在STW(全局暫停)期間,以更緊湊的方式,將正在使用中的內(nèi)存移動(復(fù)制)到堆中的其他位置,從而制造出大片的空閑內(nèi)存區(qū)域。當(dāng)內(nèi)存分配請求無法滿足時(shí)就會發(fā)生STW暫停,然后JVM完全停止應(yīng)用程序運(yùn)行,投入盡可能多的處理器線程,讓垃圾回收算法執(zhí)行內(nèi)存壓縮工作,然后分配請求的內(nèi)存,最后恢復(fù)應(yīng)用程序執(zhí)行。
Parallel GC也是一個(gè)分代回收器,旨在最大化垃圾回收效率。本文稍后會詳細(xì)討論分代式回收的思想。
G1 GC是JDK 9以后的默認(rèn)回收期。G1試圖平衡吞吐量和延遲。一方面,在STW暫停期間,依然會利用分代繼續(xù)執(zhí)行內(nèi)存回收工作,從而最大化效率,這一點(diǎn)和Parallel GC相同;但是,它還會盡可能避免在暫停期間執(zhí)行需要較長時(shí)間的操作。
G1的長時(shí)間操作會與應(yīng)用程序并行進(jìn)行,即通過多線程方式,在應(yīng)用程序運(yùn)行時(shí)執(zhí)行。這樣可以大幅度減少暫停,代價(jià)是整體的吞吐量會降低一點(diǎn)。
ZGC和Shenandoah GC專注于用吞吐量換延遲。這兩種回收器會嘗試在不進(jìn)行明顯的暫停的前提下,完成所有垃圾回收工作。目前,這兩者都不是分代式的。它們的非實(shí)驗(yàn)性版本分別于JDK 15和JDK 12引入。
Serial GC專注于內(nèi)存大小和啟動時(shí)間。這個(gè)GC像是更簡單、更慢的Parallel GC,它在STW暫停期間僅使用一個(gè)線程完成所有工作。堆也是按照分代組織的。但是Serial GC占用的內(nèi)存更小、啟動速度更快。由于它更簡單,所以更適合小型、短時(shí)間運(yùn)行的應(yīng)用程序。
OpenJDK還提供了另一個(gè)名為Epsilon的GC。為什么沒有在表1中列出呢?因?yàn)镋psilon只執(zhí)行內(nèi)存分配,從不進(jìn)行內(nèi)存回收,因此不滿足GC的所有條件。但是,Epsilon適合一些非常特殊的應(yīng)用程序。
G1 GC簡介
G1 GC于JDK 6 update 14作為實(shí)驗(yàn)特性引入,從JDK 7 update 4開始正式支持。從JDK 9開始,G1由于其多用性,成了HotSpot JVM的默認(rèn)垃圾回收器:它非常穩(wěn)定、成熟,維護(hù)也非?;钴S,而且一直在改進(jìn)。
那么,G1是如何在吞吐量和延遲之間進(jìn)行平衡的呢?
一項(xiàng)關(guān)鍵技術(shù)就是分代垃圾回收。該技術(shù)利用了一個(gè)特點(diǎn):最近分配的對象很可能可以立即回收(即它們“死亡”得更快)。所以G1(以及其他分代式GC)將Java的堆分為兩個(gè)區(qū)域:一個(gè)叫做“青年代”,用于存放剛剛分配的對象;另一個(gè)叫做“老年代”,用于存放經(jīng)歷了幾次垃圾回收后依然存活的對象,從而減少回收時(shí)所需的操作。
通常,青年代要比老年代小得多。因此,回收青年代的開銷更小,再加上G1這種跟蹤式的垃圾回收器在回收青年代對象時(shí)通常只會處理活躍對象,這就意味著青年代的垃圾回收一般非常快,而且能回收大量內(nèi)存。
在某個(gè)時(shí)間點(diǎn),長時(shí)間存活的對象會被移動到老年代中。
因此,隨著老年代不斷增長,我們也需要對其進(jìn)行垃圾回收。由于老年代一般很大,而且通常包含相當(dāng)多的活躍對象,對其進(jìn)行回收需要花費(fèi)很長時(shí)間。(例如,Parallel GC的完全回收過程通常需要消耗青年代回收數(shù)倍的時(shí)間。)
因此,G1將老年代垃圾回收過程分成了兩個(gè)階段。
G1首先跟蹤活躍對象,這一操作與Java應(yīng)用程序并行進(jìn)行。這樣,從老年代回收內(nèi)存的大量操作就不需要在垃圾回收暫停期間執(zhí)行了,從而減小延遲。不過,實(shí)際的內(nèi)存回收操作如果一次性完成的話,對于大型應(yīng)用程序的堆而言,依然需要大量時(shí)間。
因此,G1會增量式地從老年代回收內(nèi)存。在跟蹤了活躍對象之后,在接下來的幾次對青年代進(jìn)行回收的同時(shí),G1會額外對老年代中的一小部分進(jìn)行壓縮,這樣長期即可達(dá)到對年長對象進(jìn)行回收的效果。
增量地對年長對象進(jìn)行回收,比一次性回收(如Parallel GC的做法)的效率略低,因?yàn)楦檶ο箨P(guān)系圖總會不準(zhǔn)確,而且增量回收所需的數(shù)據(jù)結(jié)構(gòu)的管理也需要額外的時(shí)間和空間開銷,但這種方式可以有效減小暫停的時(shí)長。大致來看,增量式垃圾回收所需的時(shí)長基本上等于只回收青年代的算法在暫停中所花費(fèi)的時(shí)長。
此外,你還可以通過MaxGCPauseMillis命令行選項(xiàng)設(shè)置兩種垃圾回收算法的暫停時(shí)長的目標(biāo)。G1會盡可能將暫停時(shí)長保持在目標(biāo)以下。默認(rèn)的時(shí)長為200毫秒,這個(gè)值也許不適合你的應(yīng)用程序,但它只是最大值的目標(biāo)。G1會盡可能將暫停時(shí)長控制在該值以下。因此,改善暫停時(shí)長的第一步,可以從減小 MaxGCPauseMillis 開始。
從JDK 8到JDK 18的進(jìn)步
介紹完了OpenJDK的GC,我們來進(jìn)一步看看在過去10次JDK發(fā)布中,GC在吞吐量、延遲和內(nèi)存大小三個(gè)性能度量方面的進(jìn)步。
G1的吞吐量增長。為了演示吞吐量和延遲方面的進(jìn)步,本文采用了SPECjbb2015基準(zhǔn)測試。SPECjbb2015是一個(gè)衡量Java服務(wù)器性能的常用業(yè)界測試,它包含了一系列各種各樣的操作。該測試包含兩個(gè)度量:
maxjOPS是系統(tǒng)能夠提供的最大事務(wù)數(shù)量。這是吞吐量的度量指標(biāo)。
criticaljOPS測量在幾個(gè)特定的服務(wù)級別協(xié)議(SLA)下的吞吐量,比如從10毫秒到100毫秒的響應(yīng)時(shí)間。
本文采用maxjOPS作為比較不同JDK版本的吞吐量的基準(zhǔn),采用實(shí)際暫停時(shí)長的改進(jìn)量作為比較延遲的基準(zhǔn)。雖然criticaljOPS也表明了暫停時(shí)長引起的延遲,但該指標(biāo)還包含其他來源的延遲。直接比較暫停時(shí)長可以避免這個(gè)問題。
圖2展示了G1在組合模式下在一個(gè)16GB的Java堆上的maxjOPS結(jié)果,圖中給出了JDK 8、JDK 11和JDK 18的對比??梢钥闯觯琂DK版本越新,吞吐量得分就越高。JDK 11比JDK 8高出了約5%,而JDK 18高出了約18%。簡單來說,JDK版本越新,用于應(yīng)用程序?qū)嶋H工作的資源就越多。

圖2. G1d的吞吐量增長,利用SPECjbb2015的maxjOPS測量
下面,我們著重討論垃圾回收的改進(jìn)對于吞吐量增長的貢獻(xiàn)。但是,其他的一般性改進(jìn)(如代碼編譯)也對垃圾回收的性能——特別是吞吐量的增長——有很大的貢獻(xiàn),所以垃圾回收的改進(jìn)并不是唯一的貢獻(xiàn)者。
JDK 9之前的一個(gè)重大改進(jìn)是G1采用了懶惰式老年代回收,它會盡可能推遲回收操作。
在JDK 8中,用戶需要手動設(shè)置G1何時(shí)應(yīng)該對老年代回收中的活躍對象進(jìn)行并行跟蹤。如果時(shí)機(jī)設(shè)置得太早,JVM在回收操作開始之前,并沒有用完所有分配給老年代的堆內(nèi)存,如此老年代中的對象并沒有得到足夠多的時(shí)間從而變成可回收的狀態(tài)。因此,G1不僅需要更多的處理資源來分析其活躍狀態(tài)(因?yàn)樵S多數(shù)據(jù)依然處于活躍中),還要做許多額外的工作才能從老年代中釋放內(nèi)存。
另一個(gè)問題是,如果開始老年代回收的時(shí)機(jī)太晚,JVM就可能會耗盡內(nèi)存,從而導(dǎo)致內(nèi)存回收過程極其緩慢。從JDK 9開始,G1會自動決定開始老年代跟蹤的最佳時(shí)機(jī),甚至還會自動適配應(yīng)用程序的行為。
JDK 9中實(shí)現(xiàn)的另一個(gè)思想涉及到G1對于老年代中的大型對象的回收頻率比其他對象高的現(xiàn)象。與分代的思想類似,這是另一個(gè)投入產(chǎn)出比很高的想法。畢竟,大型對象所占用的內(nèi)存空間很多。在某些應(yīng)用程序中(盡管不太常見),該方法甚至能大幅度減少垃圾回收的次數(shù),并降低整體的暫停時(shí)長,使G1的吞吐量大大超過Parallel GC。
一般來說,每次發(fā)布都會包含一些改進(jìn),減小垃圾回收在執(zhí)行同樣操作時(shí)的暫停時(shí)長。這樣就會自然地改善吞吐量。還有許多可以寫在本文中的改進(jìn),接下來我們在討論延遲改進(jìn)時(shí)會提到一些。
與Parallel GC類似,從JDK 14開始,G1在Java堆上分配內(nèi)存時(shí),可以獨(dú)立地感知非統(tǒng)一性內(nèi)存訪問(NUMA)。從那時(shí)起,在擁有多內(nèi)存插槽且各個(gè)內(nèi)存的訪問時(shí)間不一致的機(jī)器上(也就是說內(nèi)存訪問與內(nèi)存插槽有關(guān),即某些內(nèi)存訪問更慢),G1會盡可能利用本地性。
有了NUMA感知后,G1 GC會假設(shè)在某個(gè)內(nèi)存節(jié)點(diǎn)上(由單個(gè)線程或線程組)分配的對象基本上被來自同一個(gè)節(jié)點(diǎn)的其他對象引用。因此,當(dāng)對象屬于青年代時(shí),G1會將對象保持在同一節(jié)點(diǎn)上,甚至還會將老年代中的長時(shí)間生存的對象分布到不同節(jié)點(diǎn)上,以最小化訪問時(shí)間的不一致性。這與Parallel GC的實(shí)現(xiàn)類似。
還有一個(gè)我想討論的改進(jìn)是關(guān)于一些罕見情況的,比如完整回收。正常情況下,G1會調(diào)整內(nèi)部參數(shù),盡力避免完整回收,但是在一些極端情況下,G1會在暫停期間進(jìn)行完整回收。直到JDK 10之前,該算法都是單線程的,所以非常慢。而目前的實(shí)現(xiàn)與Parallel GC的完整回收過程不相上下。它依然很慢,依然應(yīng)當(dāng)盡力避免,但比以前已經(jīng)好多了。
Parallel GC的吞吐量增長。關(guān)于Parallel GC,圖3給出了從JDK 8到JDK 18中maxjOPS的改進(jìn)結(jié)果,堆的設(shè)置與之前的測試相同。同樣,即使是Parallel GC,僅僅替換JVM也可以獲得大約2%的吞吐量提升,最好情況下甚至能提升10%。提升比G1小,因?yàn)镻arallel GC原本的起點(diǎn)就很高,因此增長較小。

圖3. Parallel GC的吞吐量增長,用SPECjbb2015的maxjOPS度量
G1的延遲改進(jìn)。為了演示HotSpot JVM GC在延遲方面的改進(jìn),本節(jié)采用了SPECjbb2015基準(zhǔn)測試,負(fù)載固定,然后測量其暫停時(shí)長。Java堆設(shè)置為16GB。表2總結(jié)了暫停時(shí)長的平均值和第99百分位值(P99),以及在200毫秒的默認(rèn)暫停時(shí)長目標(biāo)值下,不同JDK的相對暫??倳r(shí)長。
表2 默認(rèn)的200毫秒暫停時(shí)長下的延遲改進(jìn)

JDK 8的暫停平均時(shí)長為124毫秒,P99為176毫秒。JDK 11將平均時(shí)長提高到了111毫秒,P99提高到了134毫秒,總體減少了15.8%的暫停時(shí)長。JDK 18再次顯著改善,平均時(shí)長減少到了89毫秒,P99減小到了104毫秒,總時(shí)長減小了34.4%。
我擴(kuò)展了試驗(yàn)范圍,增加了JDK 18下暫停時(shí)長設(shè)置為50毫秒,因?yàn)橹半S意設(shè)置的-XX:MaxGCPauseMillis為200毫秒還是太長了。平均來看,G1達(dá)到了暫停時(shí)長的目標(biāo),P99垃圾回收暫停時(shí)長為56毫秒(見表3)??傮w上,與JDK 8相比,暫?;ㄙM(fèi)的總時(shí)間并沒有增加太多(0.06%)。
換句話說,將JDK 8 JVM替換成JDK 18 JVM,就能獲顯著降低平均暫停時(shí)長,同時(shí)還有可能在同樣的暫停時(shí)長目標(biāo)下提升吞吐量;或者將G1的暫停時(shí)長保持在更低的水平(50毫秒),而暫??倳r(shí)長保持不變,同時(shí)保持相同的吞吐量。
表3. 將暫停時(shí)長目標(biāo)設(shè)置為50毫秒后的延遲改進(jìn)

表3的結(jié)果是自從JDK 8以來大量改進(jìn)的結(jié)果。下面是最值得一提的改進(jìn)。
降低延遲的許多改進(jìn)都用在了減小收集老年代對象所需的元數(shù)據(jù)上?!坝涀〉募稀保╮emembered sets)的數(shù)據(jù)結(jié)構(gòu)得到了大幅度刪減,部分原因是數(shù)據(jù)結(jié)構(gòu)的精簡,另一部分是不存儲永遠(yuǎn)不會用到的數(shù)據(jù)。在今天的計(jì)算機(jī)體系架構(gòu)中,減小元數(shù)據(jù)意味著更小的內(nèi)存訪問開銷,能夠帶來性能的提升。
有關(guān)“記住的集合”的另一個(gè)方面是,人們改進(jìn)了查找指向堆中當(dāng)前被移動的區(qū)域的引用的算法,使其更容易并行化。G1不再并行遍歷整個(gè)數(shù)據(jù)結(jié)構(gòu)并在內(nèi)層循環(huán)中過濾掉重復(fù)數(shù)據(jù),而是分別并行地過濾掉重復(fù)數(shù)據(jù),再并行地處理剩余數(shù)據(jù)。這樣可以讓兩個(gè)步驟都更有效、更容易并行化。
進(jìn)一步,處理記住的集合的過程也被仔細(xì)分析,刪減了不必要的代碼,優(yōu)化了常用路徑。
JDK 8之后的另一個(gè)焦點(diǎn)是,通過一個(gè)暫停來改進(jìn)任務(wù)的并行化。人們嘗試將任務(wù)的多個(gè)階段并行化,或?qū)⑤^小的順序階段變成更大的并行階段,以此避免不必要的同步,從而改進(jìn)并行化。人們在這方面投入了大量資源來改進(jìn)并行階段的負(fù)載平衡性,這樣如果某個(gè)線程沒有任務(wù)時(shí),它會嘗試從其他線程那里獲取任務(wù)。
此外,后續(xù)的JDK開始著手更罕見的情況,其中之一就是內(nèi)存移動失?。╡vacuation failure)。如果會在垃圾回收時(shí),沒有足夠的空間復(fù)制對象時(shí),就會發(fā)生內(nèi)存移動失敗。
ZGC的垃圾回收暫停。如果你的應(yīng)用程序需要更短的垃圾回收暫停時(shí)長,可以參考表4,該表比較了G1與另一個(gè)專注于暫停時(shí)長的垃圾回收期ZGC。該表采用的負(fù)載與前面相同。最右邊一列給出了ZGC的暫停時(shí)長。
表4. ZGC與G1的延遲比較

ZGC實(shí)現(xiàn)了亞毫秒級別的暫停時(shí)長目標(biāo),它的全部內(nèi)存回收工作都與應(yīng)用程序并行執(zhí)行。只有部分不重要的工作依然需要暫停??梢韵胂螅@些暫停非常短暫,在上述情況下,暫停時(shí)長甚至遠(yuǎn)遠(yuǎn)低于ZGC聲稱的毫秒級別。
G1的內(nèi)存占用改進(jìn)。本文的最后一項(xiàng)指標(biāo)就是G1垃圾回收算法的內(nèi)存占用方面的改進(jìn)。此處,算法的內(nèi)存大小指的是垃圾回收算法為了正常工作,在正常的Java堆之外所需的額外內(nèi)存大小。
對于G1來說,除了依賴于Java堆大小的靜態(tài)數(shù)據(jù)(大小大約為Java堆尺寸的3.2%),另一個(gè)主要的內(nèi)存消耗來源是“記住的集合”,它負(fù)責(zé)分代垃圾收集,以及老年代的增量垃圾收集處理。
會給G1的記住的集合帶來壓力的應(yīng)用之一是對象緩存。每當(dāng)對象緩存增加或刪除新的緩存項(xiàng)時(shí),都會在堆上的老年代中,不斷生成區(qū)域之間的引用。
圖4展示了從JDK 8到JDK 18中,G1的原生內(nèi)存占用情況,測試應(yīng)用程序?qū)崿F(xiàn)了一個(gè)對象緩存:對象表示緩存信息,對象可以被查詢、添加,并以最近最少使用(LRU)的方式從一個(gè)更大的堆中刪除。本例中的Java堆為20GB,使用了JVM的原生內(nèi)存跟蹤(NMT)機(jī)制來確定內(nèi)存使用情況。

圖4. G1 GC的原生內(nèi)存大小
在JDK 8中,經(jīng)過了短暫的預(yù)熱階段后,G1原生內(nèi)存使用穩(wěn)定在5.8GB左右。JDK 11在此基礎(chǔ)上,將原生內(nèi)存代銷降低到了4GB左右;JDK 17進(jìn)一步改進(jìn)到1.8GB,而JDK 18穩(wěn)定在1.25GB。額外內(nèi)存使用量從JDK時(shí)代的30%堆大小降低到了JDK 18時(shí)代的6%左右。
如前所示,這些改進(jìn)并沒有造成吞吐量下降或延遲提升。實(shí)際上,G1 GC減小元數(shù)據(jù),也給其他度量帶來了提升。
從JDK 8到JDK 18,這些改進(jìn)的主要原則是,將垃圾回收元數(shù)據(jù)嚴(yán)格維持在僅保存必須數(shù)據(jù)的限度。因此,G1會并行地重建并管理內(nèi)存,盡快釋放數(shù)據(jù)。JDK 18對元數(shù)據(jù)的表現(xiàn)方式和存儲也進(jìn)行了改進(jìn),存儲得更緊密,因此有效降低了內(nèi)存大小。
圖4還表明,在新版的JDK中,G1更為積極,會主動查找穩(wěn)態(tài)操作的高峰和低谷中的差異,更積極地將內(nèi)存交還給操作系統(tǒng)。在最新的版本中,G1甚至?xí)⑿袌?zhí)行該操作。
垃圾回收的未來
盡管很難預(yù)測未來會怎樣、以后會有多少垃圾回收方面的項(xiàng)目,但G1很可能會在HotSpot JVM中實(shí)現(xiàn)下面這些改進(jìn)。
人們在努力解決的問題之一是,在原生代碼使用Java對象時(shí),會阻止垃圾回收的進(jìn)行。如果有任何區(qū)域引用了原生代碼中使用的Java對象,觸發(fā)垃圾回收的Java線程就必須等待。最糟糕的情況下,原生代碼甚至?xí)柚估厥臻L達(dá)數(shù)分鐘。這會導(dǎo)致開發(fā)人員完全避免使用原生代碼,從而大幅度影響吞吐量。JEP 423給出了解決方案,因此G1 GC很快就能解決該問題。
與Parallel GC相比,G1 GC的另一個(gè)已知問題是,它會影響吞吐率。根據(jù)用戶報(bào)告,在極端情況下,影響甚至?xí)_(dá)到10%~20%。問題的原因是已知的,人們已經(jīng)提出了幾種在不影響G1 GC其他方面的品質(zhì)的前提下的解決方案。
最近人們還發(fā)現(xiàn),暫停時(shí)長和暫停期間的負(fù)載分散的效率依然不是最優(yōu)的。
最近人們的焦點(diǎn)是將G1的最大的輔助數(shù)據(jù)結(jié)構(gòu)標(biāo)記位圖削減一半。G1算法使用了兩個(gè)位圖,用于確定哪些對象活躍,可以安全地并行檢查。一項(xiàng)仍在討論的建議表明,這兩個(gè)位圖之一可以通過其他方式取代。這就能將G1的元數(shù)據(jù)削減至一半大小,至Java堆大小的1.5%。
ZGC和Shenandoah GC也有很多在積極開發(fā)的項(xiàng)目,著眼于將這兩個(gè)垃圾回收器改造成分代式垃圾回收器。在許多應(yīng)用中,這兩個(gè)GC的單分代設(shè)計(jì)在吞吐量和即時(shí)性方面有太多的缺陷,因此需要更大的堆大小來補(bǔ)償。
總結(jié)
本文展示了HotSpot JVM垃圾回收算法從JDK 8到JDK 18的改進(jìn)。這些改進(jìn)非常顯著,所有三個(gè)性能指標(biāo),包括吞吐量、延遲和內(nèi)存大小,都得到了顯著提升。每次JDK發(fā)布新版本,都會帶來可見的提升。在可見的未來,這種趨勢仍將繼續(xù),所以請期待這些改進(jìn)吧。
感謝OpenJDK的各位貢獻(xiàn)者們付出的努力。
原文地址:https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution
推薦閱讀
你好,我是程序猿DD,10年開發(fā)老司機(jī)、阿里云MVP、騰訊云TVP、出過書創(chuàng)過業(yè)、國企4年互聯(lián)網(wǎng)6年。從普通開發(fā)到架構(gòu)師、再到合伙人。一路過來,給我最深的感受就是一定要不斷學(xué)習(xí)并關(guān)注前沿。只要你能堅(jiān)持下來,多思考、少抱怨、勤動手,就很容易實(shí)現(xiàn)彎道超車!所以,不要問我現(xiàn)在干什么是否來得及。如果你看好一個(gè)事情,一定是堅(jiān)持了才能看到希望,而不是看到希望才去堅(jiān)持。相信我,只要堅(jiān)持下來,你一定比現(xiàn)在更好!如果你還沒什么方向,可以先關(guān)注我,這里會經(jīng)常分享一些前沿資訊,幫你積累彎道超車的資本。
