<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從 JDK 8 到 JDK 18,Java 垃圾回收的十次進化

          共 8120字,需瀏覽 17分鐘

           ·

          2022-07-31 10:14

          Hollis的新書限時折扣中,一本深入講解Java基礎的干貨筆記!
          作者 | Thomas Schatzl

          譯者 | 彎月

          出品 | CSDN(ID:CSDNnews)

          經歷了數千次改進,Java的垃圾回收在吞吐量、延遲和內存大小方面有了巨大的進步。

          2014年3月JDK 8發(fā)布,自那以來JDK又連續(xù)發(fā)布了許多版本,直到今日的JDK 18是Java的第十個版本。借此機會,我們來回顧一下HotSpot JVM的垃圾回收器的發(fā)展全過程。


          關于垃圾回收、度量和取舍


          HotSpot JVM中負責管理應用程序堆的組件叫做“垃圾回收器”(Garbage Collector,即GC)。GC負責管理應用程序堆對象的整個生命周期,從應用程序分配內存到內存被回收,都由GC負責。

          從高層來看,JVM垃圾回收算法的最基本功能如下:

          • 當應用程序請求分配內存時,GC負責提供內存。提供內存的過程應盡可能快。

          • GC檢測應用程序不再使用的內存。這個操作也應當十分高效,不應消耗太多時間。這種不再使用的內存稱為“垃圾”。

          • GC將同一塊內存再次提供給應用程序,最好是“實時”,也就是要快。

          好的垃圾回收算法還有更多的需求,但這三條是最基本的,也足以支撐本文的討論了。

          滿足這些需求有很多方法,但很不幸,我們并沒有一蹴而就的方法,也沒有能一次性解決所有需求的方法。因此,JDK提供了多種垃圾回收算法以供選擇,每種算法適用于不同的場景。這些算法的實現基本上可以根據吞吐量、延遲和內存大小這三個性能度量,以及對應用程序的影響進行歸類。

          • 吞吐量指的是單位時間內能夠完成的工作量。在此語境下,垃圾回收算法的優(yōu)劣取決于能在單位時間內完成的回收工作量,這些算法可以讓Java應用程序實現更高的吞吐量。

          • 延遲指的是單次操作所需時間。垃圾回收算法需要盡可能減小延遲。在垃圾回收的語境下,關鍵點就是垃圾回收期是否會導致暫停、暫停的范圍,以及暫停的時長。

          • 在垃圾回收的語境下,內存大小指的是為了讓垃圾回收期正常工作,需要在正常的應用程序堆內存之外,再額外占用多少內存。如果GC(或更一般地,JVM)需要的內存很少,就可以給應用程序堆留出更多內存。

          這三個度量是互相關聯的:高吞吐量的垃圾回收器可能會嚴重影響延遲(但對應用程序的影響最小)。為了降低內存消耗,我們需要采用在其他度量方面不是那么出色的算法。延遲較低的回收期需要并行進行更多工作,或以更小的單位進行工作,這就會消耗更多處理器資源。

          這些關系通常可以畫成一個三角形,如圖1所示。每個垃圾回收算法占據三角形的一個角。

          圖1. GC性能度量三角

          提高GC在某方面的表現,通常會導致其他方面的表現降低。


          JDK 18中的OpenJDK GC


          OpenJDK提供了五種GC,分別專注于不同的性能度量。表1列出了GC的名稱、專注領域,以及實現特定特性所使用的核心概念。

          表1. OpenJDK的五種GC

          Parallel GC是JDK 8以及更早版本的默認回收期。它專注于吞吐量,盡快完成工作,而很少考慮延遲(暫停)。

          Parallel GC會在STW(全局暫停)期間,以更緊湊的方式,將正在使用中的內存移動(復制)到堆中的其他位置,從而制造出大片的空閑內存區(qū)域。當內存分配請求無法滿足時就會發(fā)生STW暫停,然后JVM完全停止應用程序運行,投入盡可能多的處理器線程,讓垃圾回收算法執(zhí)行內存壓縮工作,然后分配請求的內存,最后恢復應用程序執(zhí)行。

          Parallel GC也是一個分代回收器,旨在最大化垃圾回收效率。本文稍后會詳細討論分代式回收的思想。

          G1 GC是JDK 9以后的默認回收期。G1試圖平衡吞吐量和延遲。一方面,在STW暫停期間,依然會利用分代繼續(xù)執(zhí)行內存回收工作,從而最大化效率,這一點和Parallel GC相同;但是,它還會盡可能避免在暫停期間執(zhí)行需要較長時間的操作。

          G1的長時間操作會與應用程序并行進行,即通過多線程方式,在應用程序運行時執(zhí)行。這樣可以大幅度減少暫停,代價是整體的吞吐量會降低一點。

          ZGC和Shenandoah GC專注于用吞吐量換延遲。這兩種回收器會嘗試在不進行明顯的暫停的前提下,完成所有垃圾回收工作。目前,這兩者都不是分代式的。它們的非實驗性版本分別于JDK 15和JDK 12引入。

          Serial GC專注于內存大小和啟動時間。這個GC像是更簡單、更慢的Parallel GC,它在STW暫停期間僅使用一個線程完成所有工作。堆也是按照分代組織的。但是Serial GC占用的內存更小、啟動速度更快。由于它更簡單,所以更適合小型、短時間運行的應用程序。

          OpenJDK還提供了另一個名為Epsilon的GC。為什么沒有在表1中列出呢?因為Epsilon只執(zhí)行內存分配,從不進行內存回收,因此不滿足GC的所有條件。但是,Epsilon適合一些非常特殊的應用程序。


          G1 GC簡介


          G1 GC于JDK 6 update 14作為實驗特性引入,從JDK 7 update 4開始正式支持。從JDK 9開始,G1由于其多用性,成了HotSpot JVM的默認垃圾回收器:它非常穩(wěn)定、成熟,維護也非常活躍,而且一直在改進。

          那么,G1是如何在吞吐量和延遲之間進行平衡的呢?

          一項關鍵技術就是分代垃圾回收。該技術利用了一個特點:最近分配的對象很可能可以立即回收(即它們“死亡”得更快)。所以G1(以及其他分代式GC)將Java的堆分為兩個區(qū)域:一個叫做“青年代”,用于存放剛剛分配的對象;另一個叫做“老年代”,用于存放經歷了幾次垃圾回收后依然存活的對象,從而減少回收時所需的操作。

          通常,青年代要比老年代小得多。因此,回收青年代的開銷更小,再加上G1這種跟蹤式的垃圾回收器在回收青年代對象時通常只會處理活躍對象,這就意味著青年代的垃圾回收一般非常快,而且能回收大量內存。

          在某個時間點,長時間存活的對象會被移動到老年代中。

          因此,隨著老年代不斷增長,我們也需要對其進行垃圾回收。由于老年代一般很大,而且通常包含相當多的活躍對象,對其進行回收需要花費很長時間。(例如,Parallel GC的完全回收過程通常需要消耗青年代回收數倍的時間。)

          因此,G1將老年代垃圾回收過程分成了兩個階段。

          • G1首先跟蹤活躍對象,這一操作與Java應用程序并行進行。這樣,從老年代回收內存的大量操作就不需要在垃圾回收暫停期間執(zhí)行了,從而減小延遲。不過,實際的內存回收操作如果一次性完成的話,對于大型應用程序的堆而言,依然需要大量時間。

          • 因此,G1會增量式地從老年代回收內存。在跟蹤了活躍對象之后,在接下來的幾次對青年代進行回收的同時,G1會額外對老年代中的一小部分進行壓縮,這樣長期即可達到對年長對象進行回收的效果。

          增量地對年長對象進行回收,比一次性回收(如Parallel GC的做法)的效率略低,因為跟蹤對象關系圖總會不準確,而且增量回收所需的數據結構的管理也需要額外的時間和空間開銷,但這種方式可以有效減小暫停的時長。大致來看,增量式垃圾回收所需的時長基本上等于只回收青年代的算法在暫停中所花費的時長。

          此外,你還可以通過MaxGCPauseMillis命令行選項設置兩種垃圾回收算法的暫停時長的目標。G1會盡可能將暫停時長保持在目標以下。默認的時長為200毫秒,這個值也許不適合你的應用程序,但它只是最大值的目標。G1會盡可能將暫停時長控制在該值以下。因此,改善暫停時長的第一步,可以從減小 MaxGCPauseMillis 開始。


          從JDK 8到JDK 18的進步


          介紹完了OpenJDK的GC,我們來進一步看看在過去10次JDK發(fā)布中,GC在吞吐量、延遲和內存大小三個性能度量方面的進步。

          G1的吞吐量增長。為了演示吞吐量和延遲方面的進步,本文采用了SPECjbb2015基準測試。SPECjbb2015是一個衡量Java服務器性能的常用業(yè)界測試,它包含了一系列各種各樣的操作。該測試包含兩個度量:

          • maxjOPS是系統能夠提供的最大事務數量。這是吞吐量的度量指標。

          • criticaljOPS測量在幾個特定的服務級別協議(SLA)下的吞吐量,比如從10毫秒到100毫秒的響應時間。

          本文采用maxjOPS作為比較不同JDK版本的吞吐量的基準,采用實際暫停時長的改進量作為比較延遲的基準。雖然criticaljOPS也表明了暫停時長引起的延遲,但該指標還包含其他來源的延遲。直接比較暫停時長可以避免這個問題。

          圖2展示了G1在組合模式下在一個16GB的Java堆上的maxjOPS結果,圖中給出了JDK 8、JDK 11和JDK 18的對比。可以看出,JDK版本越新,吞吐量得分就越高。JDK 11比JDK 8高出了約5%,而JDK 18高出了約18%。簡單來說,JDK版本越新,用于應用程序實際工作的資源就越多。

          圖2. G1d的吞吐量增長,利用SPECjbb2015的maxjOPS測量

          下面,我們著重討論垃圾回收的改進對于吞吐量增長的貢獻。但是,其他的一般性改進(如代碼編譯)也對垃圾回收的性能——特別是吞吐量的增長——有很大的貢獻,所以垃圾回收的改進并不是唯一的貢獻者。

          JDK 9之前的一個重大改進是G1采用了懶惰式老年代回收,它會盡可能推遲回收操作。

          在JDK 8中,用戶需要手動設置G1何時應該對老年代回收中的活躍對象進行并行跟蹤。如果時機設置得太早,JVM在回收操作開始之前,并沒有用完所有分配給老年代的堆內存,如此老年代中的對象并沒有得到足夠多的時間從而變成可回收的狀態(tài)。因此,G1不僅需要更多的處理資源來分析其活躍狀態(tài)(因為許多數據依然處于活躍中),還要做許多額外的工作才能從老年代中釋放內存。

          另一個問題是,如果開始老年代回收的時機太晚,JVM就可能會耗盡內存,從而導致內存回收過程極其緩慢。從JDK 9開始,G1會自動決定開始老年代跟蹤的最佳時機,甚至還會自動適配應用程序的行為。

          JDK 9中實現的另一個思想涉及到G1對于老年代中的大型對象的回收頻率比其他對象高的現象。與分代的思想類似,這是另一個投入產出比很高的想法。畢竟,大型對象所占用的內存空間很多。在某些應用程序中(盡管不太常見),該方法甚至能大幅度減少垃圾回收的次數,并降低整體的暫停時長,使G1的吞吐量大大超過Parallel GC。

          一般來說,每次發(fā)布都會包含一些改進,減小垃圾回收在執(zhí)行同樣操作時的暫停時長。這樣就會自然地改善吞吐量。還有許多可以寫在本文中的改進,接下來我們在討論延遲改進時會提到一些。

          與Parallel GC類似,從JDK 14開始,G1在Java堆上分配內存時,可以獨立地感知非統一性內存訪問(NUMA)。從那時起,在擁有多內存插槽且各個內存的訪問時間不一致的機器上(也就是說內存訪問與內存插槽有關,即某些內存訪問更慢),G1會盡可能利用本地性。

          有了NUMA感知后,G1 GC會假設在某個內存節(jié)點上(由單個線程或線程組)分配的對象基本上被來自同一個節(jié)點的其他對象引用。因此,當對象屬于青年代時,G1會將對象保持在同一節(jié)點上,甚至還會將老年代中的長時間生存的對象分布到不同節(jié)點上,以最小化訪問時間的不一致性。這與Parallel GC的實現類似。

          還有一個我想討論的改進是關于一些罕見情況的,比如完整回收。正常情況下,G1會調整內部參數,盡力避免完整回收,但是在一些極端情況下,G1會在暫停期間進行完整回收。直到JDK 10之前,該算法都是單線程的,所以非常慢。而目前的實現與Parallel GC的完整回收過程不相上下。它依然很慢,依然應當盡力避免,但比以前已經好多了。

          Parallel GC的吞吐量增長。關于Parallel GC,圖3給出了從JDK 8到JDK 18中maxjOPS的改進結果,堆的設置與之前的測試相同。同樣,即使是Parallel GC,僅僅替換JVM也可以獲得大約2%的吞吐量提升,最好情況下甚至能提升10%。提升比G1小,因為Parallel GC原本的起點就很高,因此增長較小。

          圖3. Parallel GC的吞吐量增長,用SPECjbb2015的maxjOPS度量

          G1的延遲改進。為了演示HotSpot JVM GC在延遲方面的改進,本節(jié)采用了SPECjbb2015基準測試,負載固定,然后測量其暫停時長。Java堆設置為16GB。表2總結了暫停時長的平均值和第99百分位值(P99),以及在200毫秒的默認暫停時長目標值下,不同JDK的相對暫停總時長。

          表2 默認的200毫秒暫停時長下的延遲改進

          JDK 8的暫停平均時長為124毫秒,P99為176毫秒。JDK 11將平均時長提高到了111毫秒,P99提高到了134毫秒,總體減少了15.8%的暫停時長。JDK 18再次顯著改善,平均時長減少到了89毫秒,P99減小到了104毫秒,總時長減小了34.4%。

          我擴展了試驗范圍,增加了JDK 18下暫停時長設置為50毫秒,因為之前隨意設置的-XX:MaxGCPauseMillis為200毫秒還是太長了。平均來看,G1達到了暫停時長的目標,P99垃圾回收暫停時長為56毫秒(見表3)。總體上,與JDK 8相比,暫停花費的總時間并沒有增加太多(0.06%)。

          換句話說,將JDK 8 JVM替換成JDK 18 JVM,就能獲顯著降低平均暫停時長,同時還有可能在同樣的暫停時長目標下提升吞吐量;或者將G1的暫停時長保持在更低的水平(50毫秒),而暫停總時長保持不變,同時保持相同的吞吐量。

          表3. 將暫停時長目標設置為50毫秒后的延遲改進

          表3的結果是自從JDK 8以來大量改進的結果。下面是最值得一提的改進。

          降低延遲的許多改進都用在了減小收集老年代對象所需的元數據上。“記住的集合”(remembered sets)的數據結構得到了大幅度刪減,部分原因是數據結構的精簡,另一部分是不存儲永遠不會用到的數據。在今天的計算機體系架構中,減小元數據意味著更小的內存訪問開銷,能夠帶來性能的提升。

          有關“記住的集合”的另一個方面是,人們改進了查找指向堆中當前被移動的區(qū)域的引用的算法,使其更容易并行化。G1不再并行遍歷整個數據結構并在內層循環(huán)中過濾掉重復數據,而是分別并行地過濾掉重復數據,再并行地處理剩余數據。這樣可以讓兩個步驟都更有效、更容易并行化。

          進一步,處理記住的集合的過程也被仔細分析,刪減了不必要的代碼,優(yōu)化了常用路徑。

          JDK 8之后的另一個焦點是,通過一個暫停來改進任務的并行化。人們嘗試將任務的多個階段并行化,或將較小的順序階段變成更大的并行階段,以此避免不必要的同步,從而改進并行化。人們在這方面投入了大量資源來改進并行階段的負載平衡性,這樣如果某個線程沒有任務時,它會嘗試從其他線程那里獲取任務。

          此外,后續(xù)的JDK開始著手更罕見的情況,其中之一就是內存移動失敗(evacuation failure)。如果會在垃圾回收時,沒有足夠的空間復制對象時,就會發(fā)生內存移動失敗。

          ZGC的垃圾回收暫停。如果你的應用程序需要更短的垃圾回收暫停時長,可以參考表4,該表比較了G1與另一個專注于暫停時長的垃圾回收期ZGC。該表采用的負載與前面相同。最右邊一列給出了ZGC的暫停時長。

          表4. ZGC與G1的延遲比較

          ZGC實現了亞毫秒級別的暫停時長目標,它的全部內存回收工作都與應用程序并行執(zhí)行。只有部分不重要的工作依然需要暫停。可以想象,這些暫停非常短暫,在上述情況下,暫停時長甚至遠遠低于ZGC聲稱的毫秒級別。

          G1的內存占用改進。本文的最后一項指標就是G1垃圾回收算法的內存占用方面的改進。此處,算法的內存大小指的是垃圾回收算法為了正常工作,在正常的Java堆之外所需的額外內存大小。

          對于G1來說,除了依賴于Java堆大小的靜態(tài)數據(大小大約為Java堆尺寸的3.2%),另一個主要的內存消耗來源是“記住的集合”,它負責分代垃圾收集,以及老年代的增量垃圾收集處理。

          會給G1的記住的集合帶來壓力的應用之一是對象緩存。每當對象緩存增加或刪除新的緩存項時,都會在堆上的老年代中,不斷生成區(qū)域之間的引用。

          圖4展示了從JDK 8到JDK 18中,G1的原生內存占用情況,測試應用程序實現了一個對象緩存:對象表示緩存信息,對象可以被查詢、添加,并以最近最少使用(LRU)的方式從一個更大的堆中刪除。本例中的Java堆為20GB,使用了JVM的原生內存跟蹤(NMT)機制來確定內存使用情況。

          圖4. G1 GC的原生內存大小

          在JDK 8中,經過了短暫的預熱階段后,G1原生內存使用穩(wěn)定在5.8GB左右。JDK 11在此基礎上,將原生內存代銷降低到了4GB左右;JDK 17進一步改進到1.8GB,而JDK 18穩(wěn)定在1.25GB。額外內存使用量從JDK時代的30%堆大小降低到了JDK 18時代的6%左右。

          如前所示,這些改進并沒有造成吞吐量下降或延遲提升。實際上,G1 GC減小元數據,也給其他度量帶來了提升。

          從JDK 8到JDK 18,這些改進的主要原則是,將垃圾回收元數據嚴格維持在僅保存必須數據的限度。因此,G1會并行地重建并管理內存,盡快釋放數據。JDK 18對元數據的表現方式和存儲也進行了改進,存儲得更緊密,因此有效降低了內存大小。

          圖4還表明,在新版的JDK中,G1更為積極,會主動查找穩(wěn)態(tài)操作的高峰和低谷中的差異,更積極地將內存交還給操作系統。在最新的版本中,G1甚至會并行執(zhí)行該操作。


          垃圾回收的未來


          盡管很難預測未來會怎樣、以后會有多少垃圾回收方面的項目,但G1很可能會在HotSpot JVM中實現下面這些改進。

          人們在努力解決的問題之一是,在原生代碼使用Java對象時,會阻止垃圾回收的進行。如果有任何區(qū)域引用了原生代碼中使用的Java對象,觸發(fā)垃圾回收的Java線程就必須等待。最糟糕的情況下,原生代碼甚至會阻止垃圾回收長達數分鐘。這會導致開發(fā)人員完全避免使用原生代碼,從而大幅度影響吞吐量。JEP 423給出了解決方案,因此G1 GC很快就能解決該問題。

          與Parallel GC相比,G1 GC的另一個已知問題是,它會影響吞吐率。根據用戶報告,在極端情況下,影響甚至會達到10%~20%。問題的原因是已知的,人們已經提出了幾種在不影響G1 GC其他方面的品質的前提下的解決方案。

          最近人們還發(fā)現,暫停時長和暫停期間的負載分散的效率依然不是最優(yōu)的。

          最近人們的焦點是將G1的最大的輔助數據結構標記位圖削減一半。G1算法使用了兩個位圖,用于確定哪些對象活躍,可以安全地并行檢查。一項仍在討論的建議表明,這兩個位圖之一可以通過其他方式取代。這就能將G1的元數據削減至一半大小,至Java堆大小的1.5%。

          ZGC和Shenandoah GC也有很多在積極開發(fā)的項目,著眼于將這兩個垃圾回收器改造成分代式垃圾回收器。在許多應用中,這兩個GC的單分代設計在吞吐量和即時性方面有太多的缺陷,因此需要更大的堆大小來補償。


          總結


          本文展示了HotSpot JVM垃圾回收算法從JDK 8到JDK 18的改進。這些改進非常顯著,所有三個性能指標,包括吞吐量、延遲和內存大小,都得到了顯著提升。每次JDK發(fā)布新版本,都會帶來可見的提升。在可見的未來,這種趨勢仍將繼續(xù),所以請期待這些改進吧。

          感謝OpenJDK的各位貢獻者們付出的努力。

          原文地址:https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution


          我的新書《深入理解Java核心技術》已經上市了,上市后一直蟬聯京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~


          長按掃碼享受6折優(yōu)惠


          往期推薦

          分布式事務處理方案大 PK!


          面試官:生成訂單30分鐘未支付,則自動取消,該怎么實現?


          MySQL 啥時候用表鎖,啥時候用行鎖?




          有道無術,術可成;有術無道,止于術

          歡迎大家關注Java之道公眾號


          好文章,我在看??

          瀏覽 19
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色A片播放 | 久草资源在线观看 | 中国一级黄色免费电影 | 日韩特级片 | 日韩精品毛片在线 |