您現在的位置是:首頁 > 單機遊戲首頁單機遊戲

G1垃圾收集器簡介

簡介以下是標準young GC的外觀例項:繼續這種模式,將物件再次分配到新請求的Eden區域中

收集器是什麼

引子

最近在學習G1垃圾收集器,看到了一篇不錯的文章,翻譯了一下(英語一般,藉助google翻譯的和自己的一部分理解翻譯的。)

原文:https://www。redhat。com/en/blog/part-1-introduction-g1-garbage-collector

譯文

對於大多數人來說,Java垃圾收集器是一個很高興開展其業務的黑匣子。程式設計師開發應用程式,QE驗證功能,然後由運營團隊進行部署。在該過程中,您可以對整個堆,PermGen / Metaspace或執行緒設定進行一些調整,但是除此之外,其他一切似乎仍然可行。那麼問題就變成了,當你開始挑戰極限時會發生什麼?當這些預設值不再足夠時,會發生什麼?作為開發人員,測試人員,效能工程師或架構師,這是一套非常寶貴的技能,可以幫助您瞭解垃圾收集工作原理的基礎,以及瞭解如何收集和分析相應資料並將其轉化為有效的調優實踐。在這個系列中,我們將帶您踏上G1垃圾收集器的旅程,並將您的理解從初學者轉變為GC效能調優的大能。

我們將以最基本的主題開始本系列文章:G1(垃圾優先)收集器的作用是什麼,它實際上是如何工作的?沒有對其目標的全面瞭解,其決策方式和設計方式的全面瞭解的情況下,您正在著手實現所需的最終狀態,而無需藉助任何工具或地圖即可到達目的地。

本質上,G1收集器的目標是實現可預測的軟目標暫停時間(透過-XX:MaxGCPauseMillis定義),同時還保持一致的應用程式吞吐量。捕獲和最終目標是能夠滿足當今對高效能和需要不斷增大堆大小的多執行緒應用程式的需求,G1的一般規則是,暫停時間目標越高,可達到的吞吐量和總體延遲就越高。暫停時間目標越低,可達到的吞吐量和總體延遲就越低。垃圾收集的目標是將對應用程式執行時要求的理解結合起來,應用程式的物理特性以及對G1的理解,以調整一組選項並實現滿足您的業務需求的最佳執行狀態。重要的是要記住,調優是一個不斷髮展的過程,您可以透過重複的測試和評估來建立一組基準和最佳設定。沒有明確的指南或不可思議的選項集,您需要負責評估效能,進行增量更改並重新評估,直到達到目標為止。

就其本身而言,G1致力於以幾種不同的方式實現這些目標。首先,正如其名稱一樣,G1收集實時資料量最少的區域(垃圾優先!),並將實時資料壓縮/疏散到新區域中。其次,它使用一系列增量,並行和多相週期來實現其軟暫停時間目標。這使G1可以在定義的時間內執行所需的操作,而不管總堆大小如何。

上面,我們提到了G1中稱為“區域”(regions)的新概念。簡而言之,一個區域代表一塊分配的空間,該空間可以容納任何一代的物件,而無需與同一代其他區域保持連續性。在G1中,傳統的Young 一代和Tenured 一代仍然存在。Young 一代由Eden 空間(所有新分配的物件開始)和 Survivor 空間(活動的Eden物件在收集期間複製到其中)組成。在XX:MaxTenuringThreshold(預設為15)定義的物件被收集或足夠舊以進行升級之前,它們一直保留Survivor空間中。Tenured一代由Old Space組成,當物件到達XX:MaxTenuringThreshold時,會將物件從Survivor空間提升。當然也有例外,我們將在本文結尾處介紹。JVM啟動時將計算並定義區域大小。它基於具有儘可能接近2048個區域的原理,其中每個區域的大小在1到64 MB之間為2的冪。更簡單地說,對於12 GB的堆:

12288 MB / 2048 Regions = 6 MB - 這不是2的冪

12288 MB / 8 MB = 1536 regions - 通常太低

12288 MB / 4 MB = 3072 regions - 可以接受

根據上面的計算,預設情況下,JVM將分配3072個區域,每個區域可容納4 MB,如下圖所示。您還可以選擇透過-XX:G1HeapRegionSize顯示指定區域大小。設定區域大小時,重要的是要了解您的堆大小比將建立的區域數,因為區域越少,G1的靈活性就越小,並且掃描,標記和收集每個區域所花費的時間就越長。在所有情況下,空白區域都將新增到無序連結列表,也稱為“空閒列表”。

G1垃圾收集器簡介

關鍵在於,儘管G1是分代收集者,但空間的分配和消耗既不連續,又可以自由發展,因為它可以更好地瞭解最有效的young 與old 的比例。開始建立物件時,使用比較和交換方法從空閒列表中將一個區域分配為執行緒本地分配緩衝區(thread-local allocation buffer)(TLAB),以實現同步。然後可以在那些執行緒本地緩衝區內分配物件,而無需進行額外的同步。當該區域的空間用盡時,將選擇,分配和填充新區域。這將一直持續到累積的Eden 區域空間已滿,觸發疏散暫停(evacuation pause)(也稱為年輕集合/年輕gc /年輕暫停或混合集合/混合gc /混合暫停(collection / young gc / young pause or mixed collection / mixed gc / mixed pause))。Eden空間的累積量表示我們認為可以在定義的軟暫停時間目標內收集的區域數量。分配給Eden區域的總堆百分比可以在5%到60%之間,並且可以在每個young 集合之後根據以前的young 集合的效能進行動態調整。

這是一個將物件分配到不連續的Eden區域中的樣子的例項;

G1垃圾收集器簡介

GC pause (young); #1 [Eden: 612。0M(612。0M)->0。0B(532。0M) Survivors: 0。0B->80。0M Heap: 612。0M(12。0G)->611。7M(12。0G)] GC pause (young); #2 [Eden: 532。0M(532。0M)->0。0B(532。0M) Survivors: 80。0M->80。0M Heap: 1143。7M(12。0G)->1143。8M(12。0G)]

根據上面的“ GC暫停(年輕)”日誌,您可以看到在暫停#1中,由於Eden在總計

612.0M

(153個區域)中達到

612.0M

,觸發了疏散。當前的Eden空間已被完全疏散,為

0.0B,

並考慮到所花費的時間,還決定將Eden的總分配減少到

532.0M

或133個區域。在暫停#2中,您可以看到當我們達到

532.0M

的新限值時觸發了疏散。因為我們達到了最佳的暫停時間,所以Eden保持在

532.0M

當進行上述young GC時,將收集dead objects,並將所有剩餘的 live objects疏散並壓入Survivor 空間。G1具有由G1ReservePercent定義的顯式硬邊界(預設為10%),從而導致在疏散期間始終有一定百分比的堆可用於Survivor空間。如果沒有這個可用空間,堆可能會填滿到沒有可用疏散區域的地步。無法保證這仍然不會發生,但這就是調整的目的!該原則確保每次成功疏散後,所有先前分配的Eden地區都將返回到空閒列表,並且所有疏散的live objects都將進入Survivor空間。

以下是標準young GC的外觀例項:

G1垃圾收集器簡介

繼續這種模式,將物件再次分配到新請求的Eden區域中。當Eden的空間填滿時,會出現另一個young Gc,並且根據現有活動物件的年齡(各種物品存活了多少個young Gc),您會看到向舊城區的晉升。由於Survivor空間是young一代的一部分,dead objects將在這些年輕的暫停期間被收集或提升。

下面是一個示例,該示例顯示了當將Survivor空間中的live objects疏散並提升到舊空間中的新區域,而將Eden中的生命物撤離到新的Survivor空間區域時young Gc的情景。被刪除線表示的疏散區域現在為空,並返回到空閒列表。

G1垃圾收集器簡介

G1將繼續這種模式,直到發生以下三種情況之一:

它達到了稱為InitiatingHeapOccupancyPercent(IHOP)的可配置soft-margin。

它達到其可配置的hard-margin(G1ReservePercent)

它遇到了一個巨大的分配(這是我前面提到的例外,最後將對此進行更多介紹)。

IHOP著眼於主要觸發器,它代表一個時間點,該時間點是在一次young Gc期間計算得出的,其中舊區域中的物件數量佔總堆的45%以上(預設值)。作為每個young Gc的組成部分,這個活躍度一直在不斷地計算和評估。當這些觸發器之一被擊中時,將發出一個請求,要求開始併發標記週期。

8801。974: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 12582912000 bytes, allocation request: 0 bytes, threshold: 12562779330 bytes (45。00 %), source: end of GC] 8804。670: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested] 8805。612: [GC concurrent-mark-start] 8820。483: [GC concurrent-mark-end, 14。8711620 secs]

在G1中,併發標記基於“開始時快照(snapshot-at-the-beginning)”(SATB)的原理。這意味著,出於效率目的,如果在拍攝初始快照時存在物件,則只能將其識別為垃圾。在併發標記週期中出現的任何新分配的物件均被視為有效物件,無論其真實狀態如何。這很重要,因為完成併發標記所需的時間越長,可收集的內容與被認為是隱式存在的內容的比率就越高。如果在併發標記期間分配的物件多於最終收集的物件,則最終將耗盡堆。在併發標記週期中,您將看到young Gc繼續,因為這不是stop-the-world的事件。

下面是一個示例,該示例顯示在達到IHOP閾值並觸發併發標記時,經過young Gc之後堆的外觀。

G1垃圾收集器簡介

一旦併發標記週期完成,立即觸發一個young GC,然後進行第二種疏散,稱為混合GC。混合GC幾乎完全像young GC一樣工作,但有兩個主要區別。首先,混合GC還將收集,疏散和壓縮一組選定的舊區域。其次,混合GC不是基於young GC使用的相同疏散觸發器。他們的目標是儘可能快地,頻繁地GC。他們這樣做是為了最小化分配的伊甸園/倖存者區域的數量,以便最大化在軟暫停目標中選擇的舊區域的數量。

8821。975: [G1Ergonomics (Mixed GCs) start mixed GCs, reason: candidate old regions available, candidate old regions: 553 regions, reclaimable: 6072062616 bytes (21。75 %), threshold: 5。00 %]

上面的日誌告訴我們,由於候選舊區域(553)的總回收空間為21。75%,因此開始了混合GC。此值高於G1HeapWastePercent定義的5%最低閾值(JDK8u40 +中為5%的預設閾值,JDK7中為10%的預設閾值),因此將開始混合GC。因為我們不想執行浪費的工作,所以G1堅持垃圾優先策略。根據有序列表,根據候選物件的活動物件百分比選擇它們。如果舊區域的活動物件少於G1MixedGCLiveThresholdPercent定義的百分比(在JDK8u40 +中預設為85%,在JDK7中預設為65%),則將其新增到列表中。簡而言之,如果某個舊區域的生存率大於65%(JDK7)或85%(JDK8u40 +),那麼我們不想浪費時間在此混合週期中嘗試對其進行收集和撤離。

8822。178: [GC pause (mixed) 8822。178: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 74448, predicted base time: 170。03 ms, remaining time: 829。97 ms, target pause time: 1000。00 ms]

與young GC相比,混合GC將在相同的停頓時間目標內收集所有三個世代。它透過基於G1MixedGCCountTarget的值(預設為8)增量收集舊區域來進行管理。意思是,它將用G1MixedGCCountTarget除以候選Old區域的數量,並嘗試在每個週期中至少收集那麼多區域。每個週期結束後,將重新評估舊區域的活力。如果可回收空間仍大於G1HeapWastePercent,則混合收集將繼續。

8822。704: [G1Ergonomics (Mixed GCs) continue mixed GCs, reason: candidate old regions available, candidate old regions: 444 regions, reclaimable: 4482864320 bytes (16。06 %), threshold: 10。00 %]

此圖表示一個混合GC。所有Eden地區都被收集並疏散到Survivor地區,並且根據年齡,所有Survivor地區都將被收集,並且有足夠存續期的live objects被提升到新的Old 地區。同時,還將收集Old 區域的選定子集,並將所有剩餘的活動物件壓縮到新的Old 區域中。壓實和抽空過程可顯著減少碎片並確保維持足夠的自由區域。

G1垃圾收集器簡介

此圖表示混合收集完成後的堆。收集了所有Eden地區,並且有live objects駐留在新分配的Survivor地區。收集現有的Survivor區域,並將live objects提升到新的Old 區域。收集的Old 區域集將返回到空閒列表,所有剩餘的live objects將被壓縮到新的Old 區域中。

G1垃圾收集器簡介

混合GC將繼續進行,直到全部八個完成或可回收百分比不再達到G1HeapWastePercent。從那裡,您將看到混合的收集週期完成,並且以下事件將返回到標準的年輕收集。

8830。249: [G1Ergonomics (Mixed GCs) do not continue mixed GCs, reason: reclaimable percentage not over threshold, candidate old regions: 58 regions, reclaimable: 2789505896 bytes (9。98 %), threshold: 10。00 %]

現在我們已經涵蓋了標準用例,讓我們跳回討論我前面提到的異常。它適用於物件大小大於單個區域50%的情況。在這種情況下,物件被認為是巨大的,並透過執行專門的巨大分配進行處理。

Region Size: 4096 KB

Object A: 12800 KB

Result: Humongous Allocation across 4 regions

此圖概述了跨越4個連續區域的12。5 MB物件的龐大分配。

G1垃圾收集器簡介

大量分配表示單個物件,因此必須將其分配到連續空間中。這會導致明顯的碎片。

大型物件被直接分配到上一代中的特殊大型區域。這是因為在年輕一代中疏散和複製此類物體的成本可能太高。

即使有問題的物件只有12。5 MB,它也必須佔用四個完整區域,佔總使用量的16 MB。

無論是否滿足IHOP標準,大量分配總是會觸發併發標記週期。

幾個龐大的物件可能不會引起問題,但是對它們的穩定分配可能會導致大量的堆碎片和明顯的效能影響。在JDK8u40之前,大型物件只能透過Full GC收集,因此影響JDK7和早期JDK8使用者的可能性很大。這就是為什麼同時瞭解應用程式生成的物件大小和G1為區域大小定義的內容至關重要的原因。即使在最新的JDK8中,如果您要進行大量分配,最好還是評估並儘可能減少分配。

4948。653: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation] 7677。280: [G1Ergonomics (Concurrent Cycles) do not request concurrent cycle initiation, reason: still doing mixed collections, occupancy: 14050918400 bytes, allocation request: 16777232 bytes, threshold: 12562779330 32234。274: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 12566134784 bytes, allocation request: 9968136 bytes, threshold: 12562779330 bytes (45。00 %), source: concurrent humongous allocation]

最後,不幸的是,G1還必須處理可怕的Full GC。儘管G1最終試圖避免使用Full GC,但它們仍然是一個嚴峻的現實,尤其是在不正確的環境中。鑑於G1的目標是更大的堆大小,因此Full GC的影響對於飛行中的處理和SLA可能是災難性的。主要原因之一是Full GC在G1中仍然是單執行緒操作。檢視原因,第一個也是最可避免的與元空間有關。

[Full GC (Metadata GC Threshold) 2065630K->2053217K(31574016K), 3。5927870 secs]

前期是要更新到JDK8u40 +,其中類解除安裝不再需要完整的GC!您可能仍會在Metaspace上遇到Full GC,但這將與UseCompressedOops和UseCompressedClassesPointers或併發標記所需的時間有關(我們將在以後的文章中討論)。

後兩個原因是真實的,通常是不可避免的。作為工程師,我們的工作是透過調整和評估產生我們要收集的物件的程式碼來盡最大努力來延遲並避免這些情況。第一個主要問題是“空間耗盡”事件,然後是完整GC。此事件說明了撤消失敗,在該撤消失敗中,無法再擴充套件堆,並且沒有可用的區域來容納撤退。您還記得嗎,我們之前討論了由G1ReservePercent定義的硬邊距。此事件表明,您要向目的地撤離的物件比預留的要多,並且堆已滿,我們沒有其他可用的區域。在某些情況下,如果JVM可以解決空間條件,那麼將不會跟隨Full GC,但是這仍然是一個非常昂貴的stop the world事件。

6229。578: [GC pause (young) (to-space exhausted), 0。0406140 secs] 6229。691: [Full GC 10G->5813M(12G), 15。7221680 secs]

如果您看到這種模式經常發生,則可以立即假設您還有很大的調優空間!第二種情況是在併發標記過程中出現Full GC。在這種情況下,我們不會疏散失敗,只是在併發標記可以完成並觸發混合收集之前耗盡堆空間。造成這種情況的兩個原因可能是記憶體洩漏,或者是生產和升級物件的速度超過了收集物件的速度。如果Full GC集合是堆的重要部分,則可以假定它與生產和升級有關。如果收集的資料很少,而您最終遇到OutOfMemoryError,則很有可能會發現記憶體洩漏。

57929。136: [GC concurrent-mark-start] 57955。723: [Full GC 10G->5109M(12G), 15。1175910 secs] 57977。841: [GC concurrent-mark-abort]

最後,我希望本文能闡明G1的設計方式以及如何進行垃圾收集決策。我希望您繼續關注本系列的下一篇文章,在本文中我們將深入探討各種選項,以收集和解釋透過高階GC日誌記錄產生的大量資料。

注:有不準確的地方,大家可以留言討論。

Top