☕【JVM技術指南】「JVM總結筆記」Java虛擬機器垃圾回收認知和調優的"思南(司南)"【下部】

李浩宇Alex 發表於 2021-09-14
Java JVM

承接上文

(完結撒花1-52系列)☕【JVM技術指南】「JVM總結筆記」Java虛擬機器垃圾回收認知和調優的"思南(司南)"【上部】

並行收集器

並行收集器(也稱為吞吐量收集器)是類似於序列收集器的分代收集器。 序列和並行收集器之間的主要區別是,並行收集器有多個執行緒,用於加速垃圾回收。

通過命令列選項 -XX:+UseParallelGC 啟用並行收集器。 預設情況下,使用此選項,次要(minor)和主要(Major GC)都將並行執行,以進一步減少垃圾回收開銷。

並行垃圾收集器執行緒數

可以使用命令列選項-XX:ParallelGCThreads=<N>控制垃圾收集器執行緒的數量。

並行收集器中分代的排列

在並行收集器中,各代的排列方式是不同的。

☕【JVM技術指南】「JVM總結筆記」Java虛擬機器垃圾回收認知和調優的"思南(司南)"【下部】

並行收集器調優(Parallel Collector Ergonomics)

當使用 -XX:+UseParallelGC 選擇並行收集器時,它支援自動調優方法,允許您指定行為,而不是分代大小和其他低階調優細節。

指定並行收集器行為的選項
  • 最大垃圾收集暫停時間: 使用命令列選項 -XX:MaxGCPauseMillis=<N>指定最大暫停時間目標,這被解釋為需要毫秒或更少的暫停時間;預設情況下,沒有最大暫停時間目標。

  • 如果指定了暫停時間目標,則會調整堆大小和與垃圾收集有關的其他引數,以使垃圾收集暫停時間短於指定值。

  • 可能並不總是能夠達到所需的暫停時間目標

這些調整可能會導致垃圾收集器降低應用程式的總吞吐量。

吞吐量

吞吐量目標是根據執行垃圾回收所花費的時間與垃圾回收之外所花費的時間(稱為應用程式時間)來度量的。目標由命令列選項 -XX:GCTimeRatio= 指定,該選項將垃圾收集時間與應用程式時間的比率設定為1 / (1 + N)。

  • 例如, -XX:GCTimeRatio=19 設定了垃圾收集佔總時間的1/20或5%的目標。 預設值為99,結果是垃圾回收時間的目標為1%
記憶體空間

使用選項 -Xmx 指定最大堆記憶體佔用,此外,收集器還有一個隱式目標,即在滿足其他目標的情況下最小化堆的大小。

並行收集器目標的優先順序

目標是最大暫停時間目標、吞吐量目標和最小佔用空間目標,目標按照這個順序實現:

  • 首先實現最大暫停時間目標。只有在滿足了這個要求之後,吞吐量目標才能實現。 同樣,只有在前兩個目標已經實現之後,才會考慮記憶體大小目標。

並行收集器預設堆大小

  • 除非在命令列中指定了初始堆大小和最大堆大小,否則將根據計算機上的記憶體量計算它們。預設的最大堆大小是實體記憶體的1/4,而初始堆大小是實體記憶體的1/64。

  • 預設分配給年輕代的最大空間是總堆大小的1/3。

並行收集器初始和最大堆大小的規範

  • 你可以使用選項和 -Xmx 指定初始堆大小和最大堆大小。

    • 如果您知道應用程式需要多少堆才能正常工作,那麼可以將 -Xms 和 -Xmx 設定為相同的值。

    • 如果您不知道,那麼 JVM 將開始使用初始堆大小,然後增加Java堆,直到找到堆使用量和效能之間的平衡。

要驗證預設值,請使用 -XX:+PrintFlagsFinal 選項並在輸出中查詢 -XX:MaxHeapSize。

例如,在 Linux 上你可以執行以下命令:

java -XX:+PrintFlagsFinal <GC options> -version | grep MaxHeapSize
過長的並行收集器時間和OutOfMemoryError

如果在垃圾回收(GC)上花費了太多時間,並行收集器將丟擲OutOfMemoryError 錯誤。

如果超過98% 的總時間用於垃圾回收,而回收的堆不到2%,則丟擲 OutOfMemoryError。此特性旨在防止應用程式在較長時間內執行,同時由於堆太小而幾乎或根本沒有進展。如果需要,可以通過向命令列新增選項-XX:-UseGCOverheadLimit來禁用此特性。

G1垃圾收集器

  • G1垃圾收集器的目標是將多處理器機器擴充套件到大量記憶體。
  • 它試圖以較高的概率滿足垃圾收集暫停時間目標,同時實現較高的吞吐量而不需要進行配置。
  • G1的目標是使用當前的目標應用程式和環境,在延遲和吞吐量之間提供最佳的平衡。

與吞吐量收集器相比,雖然G1收集器的垃圾收集暫停時間通常要短得多,但應用程式吞吐量也往往略低。

G1是預設收集器。

啟用G1

G1垃圾回收器是預設回收器,因此通常不需要執行任何其他操作,您可以通過在命令列上提供 -XX:+UseG1GC來顯式啟用它。

基本概念

G1是一個分代的、遞增的、並行的、大部分併發的、stop-the-world和疏散垃圾收集器,它監視每個stop-the-world暫停的時間目標。

  • 與其他收集器類似,G1將堆分為(虛擬的)年輕代和老年代。
  • 空間回收的努力集中在年輕代身上,這樣做效率最高,偶爾的空間回收在老年代中。
G1的設計原則
  • G1的設計原則是"首先收集儘可能多的垃圾(Garbage First)"。因此,G1並不會等記憶體耗盡(序列、並行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部採用了啟發式演算法,在老年代找出具有高收集收益的分割槽進行收集。

  • G1採用記憶體分割槽(Region)的思路,將記憶體劃分為一個個相等大小的記憶體分割槽,回收時則以分割槽為單位進行回收,存活的物件複製到另一個空閒分割槽中。由於都是以相等大小的分割槽為單位進行操作,因此G1天然就是一種壓縮方案(區域性壓縮);

同時G1可以根據使用者設定的暫停時間目標自動調整年輕代和總堆大小,暫停目標越短年輕代空間越小、總空間就越大;

  • G1雖然也是分代收集器,但整個記憶體分割槽不存在物理上的年輕代與老年代的區別,也不需要完全獨立的survivor(to space)堆做複製準備。G1只有邏輯上的分代概念,或者說每個分割槽都可能隨G1的執行在不同代之間前後切換;

  • G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,採用了混合(mixed)收集的方式。即每次收集既可能只收集年輕代分割槽(年輕代收集),也可能在收集年輕代的同時,包含部分老年代分割槽(混合收集),這樣即使堆記憶體很大時,也可以限制收集範圍,從而降低停頓。

應用程式停止的其他操作會花費更多時間,比如全域性標記之類的整堆操作會與應用程式並行執行。 為了使stop-the-world在空間回收方面的停頓時間縮短,G1逐步並行地進行空間回收。

G1通過跟蹤以前應用程式行為的資訊和垃圾收集暫停來構建相關成本的模型,從而實現可預測性。它利用這個資訊來計算停頓時所做的工作量。例如,G1首先在效率最高的區域回收空間(這些區域大部分都是垃圾,因此取名為 G1)。

G1主要通過撤離來回收空間: 在選定的記憶體區域內找到的活動物件被複制到新的記憶體區域,並在處理過程中對其進行壓縮。在完成疏散之後,以前被活動物件佔用的空間將被應用程式重用以進行分配。

G1收集器不是實時收集器。它試圖在更長的時間內以高概率實現設定的暫停時間目標,但在給定的暫停時間內並不總是絕對確定。

堆佈局

G1將堆劃分為一組大小相同的堆區域Region,每個區域都有一個連續的虛擬記憶體範圍,Region區域是記憶體分配和記憶體回收的單位。

  • 在任何給定的時間,這些區域中的每一個都可以是空的(淺灰色) ,或者分配給特定的一代,年輕的或老年的。

  • 當記憶體請求進入時,記憶體管理器分配空閒區域。記憶體管理器將它們分配給一個代,然後將它們作為可用空間返回給應用程式,應用程式可以將其分配給自己。

☕【JVM技術指南】「JVM總結筆記」Java虛擬機器垃圾回收認知和調優的"思南(司南)"【下部】

年輕代包含伊甸園區域(紅色)和倖存者區域(紅色帶有"S")。這些區域提供了與其他收集器中的相應連續空間相同的功能,不同之處在於,在G1中,這些區域通常以非連續的模式佈局在記憶體中。老區域(淺藍色)組成了老年代。對於跨越多個區域的物件,老年代區域可能非常巨大(淺藍色帶"H")。

應用程式總是分配給年輕代,即伊甸園區域,但直接分配給老年代的大型物件除外。

垃圾回收週期

在較高的水平上,G1收集器在兩個階段之間交替。只有年輕(young-only)階段包含垃圾回收,這些垃圾回收會逐漸用老年代中的物件填充當前可用的記憶體。在空間回收階段,除了處理年輕代的問題外,G1逐步收回老年代的空間。然後迴圈重新開始,只有年輕的階段。

☕【JVM技術指南】「JVM總結筆記」Java虛擬機器垃圾回收認知和調優的"思南(司南)"【下部】

下面的列表詳細描述了G1垃圾收集週期的各個階段,它們之間的停頓和過渡:

純年輕(Young-only)階段
這個階段從幾個普通(Normal)的年輕代回收開始,將物件升級到老年代。 當老年代佔有率達到一定閾值時,即初始堆佔有率閾值,純年輕(young-only)階段和空間回收(space-reclamation)階段開始轉換。此時,G1計劃一個併發啟動(Concurrent Start)年輕代回收,而不是普通(Normal)的年輕代回收。
併發啟動(Concurrent Start):這種型別的回收除了執行普通年輕代回收之外,還啟動標記(marking)過程。
重標記(Remark):此暫停將自行確定標記,執行全域性引用處理和類解除安裝,回收完全空的區域並清理內部資料結構。
清理(Cleanup):這個暫停決定了是否會真正進入空間回收階段。
空間回收(Space-reclamation)階段:這一階段包括多個混合(Mixed)回收,除了年輕代區域,還刪除老一代區域的成套活動物件。當G1認為刪除更多的老年代區域不會產生足夠的自由空間時,空間回收階段就結束了。

在空間回收之後,收集週期從另一個young-only的階段重新開始。作為備份,如果應用程式在收集存活資訊時耗盡了記憶體,G1會像其他收集器一樣執行就地stop-the-world的完全堆壓縮(Full GC)。

G1內部細節

Java堆大小調整

G1在調整Java堆大小時遵循標準規則,使用 -XX:InitialHeapSize 作為最小的 Java 堆空間, -XX:MaxHeapSize 作為最大的 Java 堆空間, -XX:MinHeapFreeRatio 作為最小的可用記憶體百分比, -XX:MaxHeapFreeRatio 用於確定調整大小後可用記憶體的最大百分比。 G1收集器僅在執行重標記(Remark) 和 Full GC 暫停期間考慮調整 Java 堆的大小。 這個過程可以從作業系統釋放記憶體或分配記憶體。

Young-Only階段代調整

G1總是在下一個突變子階段的正常年輕代回收結束時測量年輕代的大小。通過這種方式,G1可以滿足使用 -XX:MaxGCPauseTimeMillis 和 -XX:PauseTimeIntervalMillis 設定的暫停時間目標,該目標基於對實際暫停時間的長期觀察。它考慮到了同樣規模的年輕代需要多長時間才能刪除。這包括在回收過程中需要複製多少物件以及這些物件之間的互聯程度等資訊。

  • 如果沒有其他限制,那麼 G1可以在 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 確定的值之間自適應地調整年輕代大小,以滿足暫停時間的要求。

  • 或者,可以使用 -XX:NewSize 和 -XX:MaxNewSize 分別設定年輕代的最小值和最大值。

  • 注意: 只指定後面這些選項中的一個,就可以將年輕代大小精確地固定為分別使用 -XX:NewSize 和 -XX:MaxNewSize 傳遞的值。這將禁用暫停時間控制。

空間回收階段的代調整

在空間回收階段,G1試圖在一次垃圾回收暫停中最大化在老年代中回收的空間量。 年輕年代的大小設定為允許的最小值,通常由-XX:G1NewSizePercent 確定。

週期性的垃圾收集
  • 如果由於應用程式不活躍而導致長時間沒有垃圾收集,那麼虛擬機器可能會長時間保留大量未使用的記憶體,這些記憶體可以在其他地方使用。

  • 為了避免這種情況,可以強制 G1使用 -XX:G1PeriodicGCInterval 選項執行常規垃圾收集。此選項確定 G1考慮執行垃圾回收的最小間隔(毫秒)。

  • 如果自以前任何垃圾收集暫停以來已經過去了這段時間,並且沒有正在進行的併發迴圈,G1將觸發額外的垃圾回收。

確定初始堆佔用率

啟動堆佔用百分比(Initiating Heap Occupancy Percent, IHOP)是觸發初始標記回收的閾值,它被定義為老年代大小的百分比。

預設情況下,G1通過在標記週期中觀察標記需要多長時間以及在老年代中通常分配多少記憶體來自動確定最佳IHOP。這個特性稱為自適應IHOP。

如果這個特性是活動的,那麼選項 -XX:InitiatingHeapOccupancyPercent 確定初始值作為當前老年代代大小的百分比,只要沒有足夠的觀測值來很好地預測啟動堆佔用閾值。

使用 -XX:-G1UseAdaptiveIHOP 選項關閉 G1的此行為。 在這種情況下, -XX:InitiatingHeapOccupancyPercent 的值總是決定這個閾值。

標記

G1標記使用一種稱為“初始快照”(Snapshot-At-The-Beginning,SATB)的演算法。 它在初始標記暫停時拍攝堆的虛擬快照,此時所有在標記開始時處於活動狀態的物件都被認為在標記的剩餘時間處於活動狀態。這意味著,為了空間回收的目的(除了一些例外) ,在標記期間變為死的(不可到達的)物件仍然被認為是活的。與其他收集器相比,這可能會導致一些額外的記憶體被錯誤地保留。但是,SATB 可能在Remark暫停期間提供更好的延遲。在這個標記期間過於保守地考慮活動物件將在下一個標記期間被回收。

  • -XX:MaxGCPauseMillis=200 最大暫停時間的目標
  • -XX:GCPauseTimeInterval= 最大暫停時間間隔的目標。 預設情況下,G1不設定任何目標,允許 G1在極端情況下背靠背地執行垃圾收集。
  • -XX:ParallelGCThreads= 垃圾回收暫停期間用於並行工作的最大執行緒數。 這是根據虛擬機器以下列方式執行的計算機的可用執行緒數得出的: 如果程式可用的 CPU 執行緒數少於或等於8,則使用該執行緒。否則,使用執行緒數的5/8。
  • -XX:ConcGCThreads=
  • -XX:+G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=45
  • -XX:G1HeapRegionSize=
  • -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60
  • -XX:G1HeapWastePercent=5
  • -XX:G1MixedGCCountTarget=8
  • -XX:G1MixedGCLiveThresholdPercent=85
與其它收集器的比較

####### 這是G1與其他收集器之間主要區別的摘要:

  • 並行 GC 只能作為一個整體壓縮和回收老年代中的空間。
  • G1增量地將這些工作分配到多個更短的回收中。這大大縮短了暫停時間,但是卻降低了吞吐量。
  • G1併發執行部分老年代空間回收。
  • G1可能比上述收集器顯示更高的開銷,由於併發性而影響吞吐量。
  • ZGC針對非常大的堆,目的是以更高的吞吐量成本提供更小的停頓時間。
  • 由於它的工作原理,G1有一些獨特的機制來提高垃圾回收效率:

在任何回收過程中,G1都可以回收老年代中一些完全空置的、大的區域。 這可以避免許多其他不必要的垃圾回收,不需要太多努力就可以釋放大量空間
G1可以選擇嘗試同時對Java堆上的重複字串進行重複資料刪除。
從老年代回收空的大型物件始終處於啟用狀態。

您可以使用 -XX:-G1EagerReclaimHumongousObjects 選項禁用此功能。 預設情況下禁用字串重複資料刪除。 您可以使用選項 -XX:+G1EnableStringDeduplication 啟用它。

Z垃圾收集器

Z垃圾收集器(ZGC)是一個可伸縮的低延遲垃圾收集器。ZGC併發地執行所有昂貴的工作,而不需要停止應用程式執行緒的執行超過10ms,這使得它適合於需要低延遲或使用非常大的堆(TB級)的應用程式。

Z垃圾收集器是一個實驗性特性,可以通過命令列選項 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC 啟用。

設定堆大小

ZGC最重要的調優選項是設定最大堆大小(-Xmx)。

設定併發GC執行緒數

可能需要考慮的第二個調優選項是設定併發GC執行緒的數量(-XX:ConcGCThreads)。

顯式垃圾回收

應用程式與垃圾回收互動的另一種方式是使用 System.gc() 顯式呼叫full垃圾回收。

類後設資料(Class Metadata)

Java類在 Java Hotspot虛擬機器中有一個內部表示,稱為類後設資料。