WebSphere Application Server V8 中的垃圾收集,第 2 部分: 新的平衡垃圾收集選項

簡介

在 Java™ 虛擬機 (JVM) 中,垃圾收集器減輕了應用程序開發人員的內存管理負擔。垃圾收集 (GC) 是一個自動化系統,同時處理 Java 對象的內存分配和回收。這有助於降低應用程序開發的複雜性,但代價是可能在應用程序的整個生命週期中表現爲不均衡的性能,以及可能影響應用程序響應能力的太長的暫停。

本系列的 第 1 部分 介紹了 IBM WebSphere Application Server V8 中可用的垃圾收集策略,以及配置新的默認分代策略的信息。本文探討平衡垃圾收集策略,一種 WebSphere Application Server V8 中提供的新的 GC 技術,可通過命令行選項-Xgcpolicy:balanced 使用。平衡收集器旨在均衡暫停時間,減少一些通常與垃圾收集相關聯的高成本操作的開銷,如圖 1 所示。

圖 1.平衡收集器的暫停時間目標
圖 1.平衡收集器的暫停時間目標

垃圾收集成本可歸因於許多因素。一些最重要的因素包括:

  • 活動數據總大小:隨着系統中活動(即可通過一系列對象引用供應用程序訪問的)對象的數量和大小增加,跟蹤或發現這些活動對象的成本也會增加。
  • 堆碎片化:隨着對象變得可回收,必須管理關聯的內存來滿足分配需求。與跟蹤此內存,使它可用於分配以及重新定位活動對象以合併空閒內存相關的成本可能很高。
  • 分配速率:分配對象的速度,以及它們的大小,將決定空閒內存的消耗率,進而決定垃圾收集的頻率。具有高速分配能力的應用程序可能會更頻繁地出現破壞性的 GC 暫停。
  • 並行性:垃圾收集器可以使用多個線程來執行垃圾收集。隨着更多線程並行完成垃圾收集,垃圾收集暫停時間減少。

最近的 Java 應用程序中的一種常見模式是使用堆中的數據存儲。示例包括內存型數據庫或低延遲緩存,常常可以在 NoSQL 解決方案中看到。相比所管理的大量內存,這些部署通常具有相對較少的可用核心數量。這些應用程序中的響應能力至關重要。較長的 GC 暫停時間可能中斷數據網格系統中保持活動的檢測信號,導致數據網格中的節點出現故障的錯誤結論,從而強制節點重啓。這會導致糟糕的響應能力,浪費的帶寬,因爲網格節點要重新啓動並重新填充;還增加了仍然活動的節點的壓力,因爲它們需要盡力處理增加的負載。

現有的垃圾收集方法使用強大的技術來有效解決如今的許多 GC 暫停時間問題。但是,不斷增加的堆大小和數據模式的變更已開始暴露這些傳統模式中的不足。

傳統的垃圾收集

存在多種現有的垃圾收集解決方案。較流行的兩種是:

整堆收集:在此方法中,JVM 通常會等到堆使用完成,然後對整個堆執行垃圾收集。主要成本與活動的應用程序集的大小成正比。其他成本可能還包括其他全局操作,比如爲了整理堆碎片而執行的整堆壓縮。

分代收集:“對象夭折” 的假設表明,在典型的應用程序中,大部分分配的對象擁有極短的壽命,可在分配後不久即收集。分代收集器通過在堆中指定用於對象分配並由特殊的收集器收集的區域(通常稱爲新空間)來利用此假設。這有望實現最佳的時間和精力投資回報,減少與整堆收集相關的總暫停時間。最終,當對象存活的時間長到足夠離開新空間時,堆的剩餘部分(稱爲舊空間)會填滿,並必須使用整堆收集器來收集。

圖 2. 新空間收集區域和大小與整堆(全局)收集對比
圖 2. 新空間收集區域和大小與整堆(全局)收集對比

有其他許多技術可幫助減輕垃圾收集中的暫停時間壓力,包括:

  • 並行性:在多核系統上使用多個 GC 線程,以更快完成 GC 操作,減少暫停時間。
  • 併發性:通過一組專用的 GC 幫助器線程來以 Java 線程形式執行 GC 操作,創建 Java 線程來幫助完成工作。
  • 增量收集:通過將工作分散到多個更短的暫停時間內,減少平均 GC 暫停時間,最終完成整個 GC。此方法通常以更短的平均暫停時間來換取較長的總暫停時間,因爲增量化操作所需的處理工作也具有開銷。

本系列第 1 部分 中已經提到,有許多強大的 GC 技術可供應用程序開發人員使用,具體取決於特定部署所需的性能特徵。

平衡垃圾收集器的目標

平衡收集器的主要目標是將全局垃圾收集的成本分擔在許多 GC 暫停上,減少整堆收集時間的影響。與此同時,每次暫停應該嘗試執行一次獨立的收集,將空閒內存返回到應用程序供直接重用。

爲了實現此目的,平衡收集器使用一種動態方法來選擇要收集的堆區域,以最大化時間和精力的投資回報。這類似於 gencon 策略方法,但更加靈活,因爲它會在每次暫停期間考慮對堆的所有部分進行收集,而不是僅考慮靜態定義的新空間。

圖 3. 動態選擇堆區域進行收集的平衡能力
圖 3. 動態選擇堆區域進行收集的平衡能力

通過刪除對要收集的堆區域的限制,平衡收集器能夠動態適應廣泛的對象使用模式。例如,在 3 分鐘後具有高對象死亡率的應用程序、每次事務要分配 100MB 的應用程序,或者在某些對象生命週期內容易產生碎片的應用程序,都可通過在合適的時刻專注於堆的合適區域來解決。

平衡收集器會基於所生成的工作量,在 GC 操作之間均勻分配暫停時間。這可能受到對象分配速率、對象存活率和堆內的碎片級別的影響。請注意,這種暫停時間的平均化只是一種盡力而爲,而不是一種實時保證。暫停時間無法保證在某個最大值之內,該技術也無法提供利用率保證。

平衡收集器基於 IBM JDK 內現有的 GC 技術的功能,包括 optavgpause、gencon 和 metronome。本文的其餘部分將介紹平衡收集器所採取的一般垃圾收集方法,用於實現這些目標的一些重要的技術,應該使用平衡收集器的場景,以及關於調節收集器以實現最佳結果的建議。

平衡收集器工作原理

本節介紹平衡收集器的操作,首先解釋如何組織堆,然後介紹平衡收集器用於收集堆和將空閒內存返回給應用程序的技術。

堆的組織

平衡收集器架構的一個基本方面(也是實現其減少較長收集時間影響的目標的關鍵)是,它是一種基於區域的垃圾收集器。區域是 Java 對象堆的一個具有明確界限的部分,它對相關內存的使用進行分類並將相關對象分組到一起。在 JVM 啓動期間,垃圾收集器將堆內存分解爲相等大小的區域,這些區域界線在 JVM 的生命週期內保持靜態。

區域是基本的垃圾收集和分配操作單元。例如,當堆被展開或收縮時,分配或釋放的內存將與區域數量相對應。儘管 Java 堆是一個連續的內存地址範圍,但該範圍內的任何區域都可根據需要分配或釋放。這使平衡收集器可比其他垃圾收集器更加動態和積極地收縮堆,這通常需要分配的堆是連續的。

還需要注意,GC 操作(比如跟蹤活動對象或壓縮)在一組區域上操作。因爲堆分區爲定義良好的區域,所以當平衡收集器在不同時間分析可用的堆數據時,收集操作可能在不同的區域集上操作。

單個區域中的對象具有某些共同特徵,比如具有類似的年齡(圖 4)。具體來講,新分配的對象(自上一個 GC 週期後分配的對象)都集中在一個所謂的 eden 區域中。eden 區域很值得注意,因爲它們始終包含在下一個收集週期中。

區域具有最大對象大小限制。對象始終分配在單個區域界限內。無法放在單個區域內的數組使用一種稱爲 arraylet 的不連續格式來表示,稍後將介紹該格式。除了 arraylet,對象從不允許跨越區域。

區域大小始終爲 2 的冪次(例如 512KB、1MB、2MB、4MB 等)。區域大小在啓動時選擇,基於最大堆大小。收集器選擇 2 的最小冪次,這將產生不到 2048 個區域,最小的區域大小爲 512KB。除了較小的堆(小於 512MB),JVM 可以擁有 1024 到 2047 個區域。

圖 4. 對象堆中存在的區域結構和特徵
圖 4. 對象堆中存在的區域結構和特徵

收集堆

就像 gencon 收集器一樣,平衡收集器利用了最新收集的對象可能快速變成垃圾的觀察結果。它通過一個完全中斷的週期來查找新創建的對象,意味着在 GC 處理的過程中,所有 Java 線程都會暫停執行。一種更加全局化的操作支持此方法,以幫助處理一組 eden 區域(始終會收集)外的對象。

有 3 種不同類型的垃圾收集週期(圖 5):

  • 部分垃圾收集 (PGC) 收集一個稱爲收集組的區域子集。PGC 是在使用平衡收集器時最常見的 GC 週期。它用於從具有高死亡率的區域收集垃圾,收集 eden 集外的區域並對其進行碎片整理。
  • 全局標記階段 (GMP) 增量式地跟蹤堆中的所有活動對象。GMP 是常見的操作,但通常沒有 PGC 常見。GMP 週期不負責將空閒內存返回到堆或壓縮碎片化的堆區域(這是 PGC 的職責)。GMP 的作用主要是爲 PGC 提供支持,細化用於確定堆的哪些區域最適合不同的收集操作的信息。
  • 全局垃圾收集 (GGC) 以完全中斷的方式標記、擦除並壓縮整個堆。GGC 通常僅用在應用程序通過調用 System.gc() 來顯式調用收集器時。它也可以在內存嚴重不足時執行,作爲在拋出OutOfMemoryError 之前釋放內存的最後補救辦法。平衡收集器的一個主要目標是避免全局垃圾收集,除非顯式調用,這些全局垃圾收集應被視爲在調節平衡收集器時發生的問題。

認識到 PGC 是獨立的完全中斷性操作之後,GMP 週期可跨越多個增量,在 Java 應用程序運行期間部分併發執行。每個 PGC 週期的目標是回收內存,它會選擇堆中要收集的區域,執行收集,將最終的空閒內存返回到應用程序做分配之用。圖 5 顯示了一個典型的時間線,描述了平衡收集器的操作模式。

圖 5. 典型的平衡收集器行爲的時間線表示
圖 5. 典型的平衡收集器行爲的時間線表示

理解部分垃圾收集的作用

每個部分垃圾收集 (PGC) 負責確保有足夠的空閒內存使應用程序繼續運行。爲此,每個 PGC 選擇將一些區域包含在收集組中。可通過 3 個主要因素來決定一個區域是否屬於收集組(圖 6):

  • Eden 區域(包含自上一次 PGC 之後分配的對象)始終包含在內,這主要是因爲它們通常具有較高的死亡率。這還使 GC 能夠分析和記錄與這些對象的引用圖和活躍度統計相關的元數據。
  • GMP 階段發現的區域,它們非常零散,且在分配、壓縮和合並時有助於將空閒內存返回給應用程序。這稱爲碎片整理。要創建空閒區域來用作 eden 區域,離不開持續碎片整理。
  • eden 區域集外的區域,它們應該具有高死亡率。GC 以對象年齡的函數的形式,收集對象平均死亡率的統計數據。基於這些統計數據,每個 PGC 動態選擇一些區域,它期望在這些區域中找到對於發現活動對象所需的工作量而言足夠的垃圾(進而釋放足夠的空閒內存)。
圖 6. 對象堆中按區域選擇的收集組
圖 6. 對象堆中按區域選擇的收集組

一個 PGC 通常採用一個類似於 gencon 策略所使用的收集器的複製收集器。這種複製方法要求 PGC 保留一定量的空閒區域,作爲從收集組撤離的活動對象的目的地。與 gencon 不同,平衡收集器不爲存活的對象預先分配內存。相反,它估計預期的存活率並使用堆中一個足夠大的空閒區域子集,以便成功複製來自收集組的所有活動對象。

在具有較高的分配易變性和死亡率的應用程序工作負載中,預計的存活對象區域大小可能比實際可用的區域更大。在這種情況下,PGC 將從一種複製機制切換到一種原地跟蹤並壓縮的方法。壓縮週期使用滑動壓縮,這可以保證成功地完成操作,而無需任何空閒的堆內存,不同於首選的複製方法。

除了在 PGC 之間切換收集模式,平衡收集器還能夠在 PGC 期間切換模式。如果評估證明存活對象內存需求不夠用,複製收集器會填充剩餘的空閒空間,然後動態過渡到原地方法。在過渡之前複製的任何對象仍然被複制,沒有成功複製的對象將在原地收集。

在所有情況下,PGC 內的所有操作都是完全中斷性的 (STW)。這意味着在 GC 週期完成之前所有 Java 線程都會暫停執行。

理解全局標記階段的作用

PGC 可以恢復較高比例的垃圾,而通過戰略性收集組決策實現相對較低的暫停時間。PGC 沒有堆生命跡象的全局知識,它用於做決定的數據將逐漸變得越來越不可靠。全局標記階段 (GMP) 負責刷新整個堆的視圖,使 PGC 能夠制定關於收集組選擇和跟上應用程序的堆使用率所需的工作的更精明決策。

GMP 在 PGC 無法及時處理拒絕進入系統的活動數據時被激活。PGC 沒有發現的垃圾會緩慢積累,堆將逐漸填滿。當 PGC 的效率惡化時,GMP 週期就會啓動。GMP 通過並行 STW 增量和併發處理的結合來在堆中標記所有活動對象。

爲了確定何時應該發起 GMP 週期,GC 預測堆被消耗的速率、全局活動對象集的大小,以及跟蹤活動對象集的成本。基於此信息,GC 計劃初始啓動點、完成 GMP 所需的增量數,以及每個增量需要完成的工作量。此計劃旨在:

  • 最小化堆應用程序的影響。
  • 在堆完全耗盡空閒內存之前完成 GMP。(更準確來講,要完成該任務,需要有足夠的空閒空間供 PGC 用於有效完成增量。)

GMP 增量大概在 PGC 週期中間安排。此外,如果在 PGC 週期結束時存在可用的處理器時間(比如空閒的核心),將在應用程序執行期間分配線程來幫助併發地完成 GMP 增量。如果併發線程能夠在其計劃的時刻之前完成下一個 GMP 增量,則不會執行額外的工作。

幕後:幫助引發這一切的關鍵機制

現在您已理解了平衡收集器所使用的核心基礎架構和方法,您應該知道支持該收集器並幫助它實現其目標的兩種重要機制:記憶集合和 arraylet。

記憶集合

PGC 要能夠準確發現所有活動的對象,收集器必須掃描所有對象 root(比如線程堆棧、永久類加載器、JNI 引用)。此外,必鬚髮現從收集組外部的對象對收集組內的對象的引用。這可以通過掃描未包含在收集組內的所有區域中的所有對象來完成,但這種方法可能效率很低。相反,不同區域中的對象之間的引用在一個稱爲 “記憶集合” 的數據結構中得到跟蹤和記錄。記憶集合用於按區域跟蹤所有傳入的引用(圖 7)。

圖 7. 記憶集合的基本結構
圖 7. 記憶集合的基本結構

引用在程序執行期間通過一個寫屏障(write barrier)來創建和發現。此過程由 JVM 處理,對於 Java 應用程序不可見,無需更改 Java 代碼。

堆外內存保留給記憶集合存儲,通常不超過當前堆大小的 4%。在每個區域所跟蹤的傳入引用的數量上也存在限制。如果達到了區域的全侷限制 (4%) 或局部限制,那麼堆區域的記憶集合的任何數據添加都將導致該區域被標記爲 “popular”。這會使該區域無法被 PGC 收集。它可能再次變爲下一個 GMP 週期後的收集候選者,這會從記憶集合刪除過期信息並更新它。

Arraylet

大部分對象都可輕鬆包含在 512KB 的最小區域大小內。但是,一些大型數組可能需要比單個區域中的可用內存更多的內存。爲了支持這種大型數組,平衡收集器爲大型數組使用了一種 arraylet 表示形式。

圖 8 表明大型數組對象顯示爲一個 spine,它是中央的對象和惟一可被堆上的其他對象引用的實體以及一系列 arraylet 葉,其中包含實際的數組元素:

圖 8. arraylet 的基本結構
圖 8. arraylet 的基本結構

arraylet 葉不會直接由其他堆對象引用,可按任何順序分散在堆中。每個葉子是一個完整區域,允許對元素位置執行簡單的計算,需要一種額外的媒介才能到達任何元素。如圖 8 所示,由於 spine 內部的碎片而導致的內存使用開銷已得到優化,方法是將最終葉子的任何尾隨數據包含在 spine 中。

因爲數組表示被 JVM 隱藏,所以 arraylet 的形狀所帶來的明顯複雜性對 Java 應用程序不可見。無需任何代碼修改或 arraylet 知識。

使用 arraylet 有許多優勢。由於堆會逐漸變成碎片,所以其他收集器策略可能會強制運行全局垃圾收集和碎片整理(壓縮)階段,以便恢復足夠的連續內存來分配大型數組。通過消除在連續內存內分配大型數組的需求,平衡垃圾收集器更容易滿足這樣的分配需求,而無需計劃外的垃圾收集,而且無需全局碎片整理也能夠滿足此需求。此外,平衡收集器從不需要在分配了 arraylet 葉之後移動它。重新定位數組的成本僅限於重新定位 spine 的成本,所以大型數組不會導致更長的碎片整理時間。

圖 9. 在碎片化的堆中將數組分配爲 arraylet
圖 9. 在碎片化的堆中將數組分配爲 arraylet

arraylet 表示形式僅用於非常大的數組。小數組在平衡收集器中具有與其他 IBM 垃圾收集器(比如 gencon 收集器)相同的表示形式。對於小數組,沒有額外的空間開銷。但是,由於 JIT 編譯的代碼在大部分情況下都需要同時包含小型和大型數組的邏輯,所以 arraylet 的使用可能導致更多的已編譯代碼。

使用 arraylet 的最明顯後果可以在 Java 本地接口 (JNI) 代碼中看到。JNI 提供了一些 API,包括 GetPrimitiveArrayCriticalReleasePrimitiveArrayCriticalGetStringCriticalReleaseStringCritical,只要可能,它們就會提供對數組數據的直接訪問。平衡收集器提供了對連續數組的直接訪問,但需要爲不連續的 arraylet 將數組數據複製到連續的堆外內存塊中,因爲 arraylet 的表示與這些 API 所需的連續表示不一致。

如果您認爲這會影響您的應用程序,還有其他一些可能的解決方案。首先,確定是否複製數組數據。前面提到的 JNI API 包含一個 isCopy 返回參數。如果 API 將此參數設置爲JNI_TRUE,那麼數組將是不連續的,將會複製數據。檢查您的 JNI 代碼,確定相關的本地函數是否可使用 Java 重寫,或者更改爲使用不同的 API,例如Get<Type>ArrayRegion。如果數據未修改,確保 ReleasePrimitiveArrayCritical 的任何調用方使用了JNI_ABORT 模式,因爲這樣會消除將數據複製回 Java 堆的需要。最後,較大的區域大小(由增長的堆大小控制)可以減少或消除 arraylet。

調節平衡收集器

平衡收集器使用一種與 gencon 收集器(如 第 1 部分 中所述)類似的收集方法,所以許多相同的技術也適用於它。但是,我們有必要列出需要考慮的一些區別:

  • eden 空間的平衡表示法類似於 gencon 新空間,但不相同。eden 空間包含收集流程總會涉及到的新創建的對象,但它們在收集之後就會成爲一般堆的一部分。相反,在轉移到舊空間之前,對象可在多次收集中保留在 gencon 新空間中。
  • 儘管存在全局跟蹤階段的平衡表示法,但平衡收集器的一個目標是通過增量收集 eden 空間外的堆並對它執行碎片整理,完全避免定期全局收集(尤其是全局壓縮)。這與 gencon 相反,後者僅關注新空間,最終會導致在舊空間耗盡時執行全局收集。
  • 長期活動的對象(比如類和字符串常量)從不會分配到 gencon 的新空間中(它們直接分配給舊空間)。平衡收集器將這些對象分配到 eden 空間中,隨後在收集週期中收集它們。

平衡收集器的基本調節選項與 gencon 相同,只是 eden 空間取代了可調節的新空間。這裏回顧一下這些選項:

  • -Xmn<size> 設置 eden 空間的建議大小,有效地設置 -Xmns -Xmnx
  • -Xmns<size> 將 eden 空間的建議初始值設置爲指定的值。
  • -Xmnx<size> 將 eden 空間的建議最大值設置爲指定的值。

請注意這些是建議的選項,只要系統能夠適應任何限制,平衡收集器就會採用這些選項。例如,如果對於建議的 eden 大小沒有足夠的堆內存,平衡收集器會將 eden 空間減小爲可獲得的大小。默認的建議 eden 大小爲當前堆大小的 1/4。

調節 den 空間的主要目標應該是包含分配給系統內一組事務的對象。因爲大部分系統都會不斷擁有許多事物(原因在於多線程處理),所以 eden 空間應該能夠在任何時刻容納所有這些事務。從調節的角度講,這意味着常規負載下從系統中的 eden 空間存活的數據量應該比 eden 空間本身的實際大小小很多。一般來說,要實現最優性能,在每次收集中從 eden 空間存活的數據量應該保持到大約 20% 或更少。一些系統可能能夠容忍超出這些邊界的參數,具體取決於堆的總大小或系統中可用 GC 線程的數量。關鍵在於使用這些指南作爲部署應用程序的起點。一般而言,用於 gencon 策略的任何 -Xmn 設置也都適用於平衡收集器。

儘管可以使用 -Xmn 自由調節 eden 空間的大小,但在這麼做時需要記住一些要點:

  • 保持 eden 較小:通過將 eden 空間縮小到最小值(基於事務大小和週轉時間),您的系統可能比它需要的更頻繁地暫停,從而降低了性能。另外,如果太頻繁地調節 eden 大小,任何行爲更改(例如,從突然出現故障的系統接管更多工作,以滿足高可用性需求)將超出 eden 空間範圍,導致欠佳的性能。
  • 保持 eden 較大:通過將 eden 空間調大,可以減少收集暫停次數(潛在地提高性能),但會減少可用於一般堆的內存量。如果一般堆相對於總體活動集合而言太小,它可能強制平衡收集器在每個 PGC 週期中增量式地收集堆的較大部分並執行碎片整理,以保持正常運行,導致較長的 GC 暫停時間和欠佳的性能。

在所有情況下都需要權衡,而且調節過程是系統的和迭代式的:

  1. 根據需要調節堆最大值 (-Xmx) 和初始 (-Xms) 內存。
  2. 在正常負載壓力下運行應用程序。
  3. 收集冗長的 GC 日誌(-verbose:gc-Xverbosegclog:<filename>)以供分析
  4. 如果有必要,使用合適的工具確定對 eden 大小的調節 (-Xmn)。
  5. 返回執行第 2 步,直到獲得滿意的性能。

儘管可以手動檢查詳細的 GC 日誌,但數據量常常非常大。Garbage Collection and Memory Visualizer (GCMV) 工具使用詳細的 GC 日誌,可視化結果,並提供數據分析(圖 10)。GCMV 報告正確的信息、錯誤的信息、需要關注的信息,以及最重要地,提供建議來幫助改進性能。

圖 10. Garbage Collection and Memory Visualizer (GCMV)
圖 10. Garbage Collection and Memory Visualizer (GCMV)

如果希望親自嘗試,以及在一些情況下通過 GCMV 從日誌中挖掘可能還不可用的信息,那麼直接檢查詳細的 GC 日誌是一種可接受的問題診斷和調節途徑。清單 1 中給出了一節詳細 GC 日誌的示例,它描述了一個典型的 PGC 週期。

清單 1. 一節詳細 GC 日誌的示例
<exclusive-start id="137" timestamp="2011-06-22T16:18:32.453" intervalms="3421.733">
  <response-info timems="0.146" idlems="0.104" threads="4" lastid="0000000000D97A00"
   lastname="XYZ Thread Pool : 34" />
</exclusive-start>
<allocation-taxation id="138" taxation-threshold="671088640"
 timestamp="2011-06-22T16:18:32.454" intervalms="3421.689" />
<cycle-start id="139" type="partial gc" contextid="0" timestamp="2011-06-22T16:18:32.454"
 intervalms="3421.707" />
<gc-start id="140" type="partial gc" contextid="139" timestamp="2011-06-22T16:18:32.454">
  <mem-info id="141" free="8749318144" total="10628366336" percent="82">
    <mem type="eden" free="0" total="671088640" percent="0" />
    <numa common="10958264" local="1726060224" non-local="0" non-local-percent="0" />
    <remembered-set count="352640" freebytes="422080000" totalbytes="424901120" 
     percent="99" regionsoverflowed="0" />
  </mem-info>
</gc-start>
<allocation-stats totalBytes="665373480" >
  <allocated-bytes non-tlh="2591104" tlh="662782376" arrayletleaf="0"/>
  <largest-consumer threadName="WXYConnection[192.168.1.1,port=1234]"
   threadId="0000000000C6ED00" bytes="148341176" />
</allocation-stats>
<gc-op id="142" type="copy forward" timems="71.024" contextid="139"
 timestamp="2011-06-22T16:18:32.527">
  <memory-copied type="eden" objects="171444" bytes="103905272"
   bytesdiscarded="5289504" />
  <memory-copied type="other" objects="75450" bytes="96864448" bytesdiscarded="4600472" />
  <memory-cardclean objects="88738" bytes="5422432" />
  <remembered-set-cleared processed="315048" cleared="53760" durationms="3.108" />
  <finalization candidates="45390" enqueued="45125" />
  <references type="soft" candidates="2" cleared="0" enqueued="0" dynamicThreshold="28"
   maxThreshold="32" />
  <references type="weak" candidates="1" cleared="0" enqueued="0" />
</gc-op>
<gc-op id="143" type="classunload" timems="0.021" contextid="139"
 timestamp="2011-06-22T16:18:32.527">
  <classunload-info classloadercandidates="178" classloadersunloaded="0"
   classesunloaded="0" quiescems="0.000" setupms="0.018" scanms="0.000" postms="0.001" />
</gc-op>
<gc-end id="144" type="partial gc" contextid="139" durationms="72.804"
 timestamp="2011-06-22T16:18:32.527">
  <mem-info id="145" free="9311354880" total="10628366336" percent="87">
    <numa common="10958264" local="1151395432" non-local="0" non-local-percent="0" />
    <pending-finalizers system="45125" default="0" reference="0" classloader="0" />
    <remembered-set count="383264" freebytes="421835008" totalbytes="424901120"
     percent="99" regionsoverflowed="0" />
  </mem-info>
</gc-end>
<cycle-end id="146" type="partial gc" contextid="139"
 timestamp="2011-06-22T16:18:32.530" />
<exclusive-end id="147" timestamp="2011-06-22T16:18:32.531" durationms="77.064" />

使用平衡收集器的時機

平衡收集器是較舊的策略(包括 gencon)的一種合適的替代品,只要環境和應用程序需要能夠適合相關利弊。一般而言,在以下情況下建議使用平衡收集器:

  • 應用程序在 64 位平臺上運行並部署了一個大於 4GB 的堆。平衡收集器是在大型堆上減少大型全局 GC 暫停時間的最佳選擇。由於與平衡收集器中使用的技術相關的開銷,小型堆可能不提供理想的部署場景。
  • 應用程序可使用 gencon 獲得出色的結果,但仍然會遇到偶爾過長的全局 GC 暫停時間,包括較長的全局壓縮時間。平衡收集器提供的平均暫停時間可能比 gencon 稍微長一些,但通過專注於新創建的對象而保留了 gencon 的優點,並且最終將通過增量收集和壓縮全局堆來避免全局收集成本。請注意,在其他 GC 策略中,由於大對象(尤其是數組)的分配,全局收集和壓縮也可能過於頻繁地發生。
  • 應用程序願意接受性能的細微降級。前面已經提到,平衡收集器的技術方法比 gencon 或其他 GC 策略複雜得多,因此,暫停時間和 Java 應用程序開銷方面的 GC 成本都要高些。儘管此開銷常常不會超過 10%,它仍然代表着不小的得失,尤其是在與 gencon 對比時。

NUMA 支持

非統一內存訪問 (NUMA) 是一種硬件架構,其中處理器和內存都組織爲稱爲節點的組。處理器訪問它們自己節點本地的內存比訪問與其他節點相關的內存更快。NUMA 可用於使用最新版 AIX®、Linux® 或 Windows® 的 System x® 和System p®。

圖 11. 示例 NUMA 配置

圖 11. 示例 NUMA 配置

平衡收集器組織內存和垃圾收集工作以充分利用 NUMA。在啓動時,收集器將每個堆區域綁定到系統的一個 NUMA 節點。堆儘可能均勻地拆分,使所有節點具有大體相同的區域數量。

JVM 中的大部分線程綁定或關聯到 NUMA 節點。線程以一種簡單的輪詢方式綁定:第一個線程綁定到第一個節點,第二個線程綁定到第二個節點,依此類推。一些線程(比如主線程和通過 JNI 附加的線程)可能不會綁定到特定節點。

因爲線程要分配對象,所以它們嘗試將這些對象放在它們的本地內存中,以更快地訪問。如果沒有足夠的本地內存,線程可以借用其他節點的內存。儘管此內存的訪問更慢,但它仍然比計劃外的 GC 或OutOfMemoryError 更適合。

在垃圾收集期間,並行 GC 線程首選與它們自己的節點相關的工作。這會使暫停時間更短。收集器還會轉移對象,以便每個對象儘可能存儲在使用該對象的線程本地的內存中。這可以帶來改進的應用程序吞吐量。

一些操作系統提供了實用程序來控制一個進程可使用哪些節點(例如 Linux 上的 numactl 或 AIX 上的 execrset)。JVM 將檢測一個節點子集是否可用,並將僅使用允許的資源。JVM 中的 NUMA 支持可使用-Xnuma:none 命令行選項禁用。

Java 應用程序還可以利用許多性能或行爲收益,但這不是轉向使用平衡收集器的必要原因:

  • 應用程序可以大量使用類卸載,無論是通過反射還是其他工具。gencon 依靠全局收集來執行類卸載和字符串常量垃圾收集,與此不同,平衡收集器能夠收集類加載器和字符串常量,只要它們位於給定的收集組中,包括夭折的、新創建的對象。這是平衡收集器的一項優勢,因爲它能夠迅速收集這些對象和與它們關聯的本地內存結構,減少總體內存壓力和開銷。
  • 應用程序部署在大型硬件(大量內存,大量核心)上,平衡收集器可以更好地利用它們。平衡收集器也將識別 NUMA 系統,部署 GC 線程和 Java 線程,以及分配堆中的對象,採用可以利用內存速度上的區別的方式。此外,有了大量的核心,平衡收集器可通過部署幫助器線程來加速收集過程,從而利用併發性(GC 工作在 Java 線程運行期間繼續運行)。
  • 應用程序使用非常大的數組,平衡收集器可使用不連續的表示形式來存儲這樣的數組。平衡收集器能夠在垃圾收集期間更有效地處理這些數組,也可以避免通過壓縮來分配大型數組。

最後,在評估平衡收集器時,還有許多因素需要考慮,這些因素可能意味着它不適合部署:

  • 平衡收集器不是一種實時的垃圾收集器,如果需要實時結果,您應該使用 IBM WebSphere Real Time for AIX 或 IBM WebSphere Real Time for Linux。儘管平衡收集器在盡力緩和 GC 暫停時間,但它最終無法保證最大的暫停時間或暫停將完全一致。應用程序生成的工作將表明 GC 暫停的頻率和持續時間。
  • 因爲基於堆在內部的組織方式,平衡收集器會使用更多的堆內存,所以它不是很適合已經很滿(超過 90% 的佔用率)的堆。由於全局收集過程的增量性質,以及區域中存在微型碎片的可能性,對非常滿的堆使用平衡收集器可能不會實現良好的暫停時間行爲,因爲它會盡力在相對較短的時間內處理整個堆,實際上使它充當着 optthruput 或 optavgpause 等全局收集器。
  • 因爲平衡收集器爲非常大的數組使用了一種不連續的表示法,所以 JNI 對這些數組的訪問可能比其他收集器更慢。如果應用程序廣泛使用 JNI 和大型數組,您也許能夠充分增加堆大小來使數組變得連續,進而改進性能。您也許還能夠修改本地代碼來減少它對大型數組的依賴性。如果這些可能性都不存在,平衡收集器可能不適合您的部署。

結束語

本文介紹了平衡 GC 技術,它可通過 IBM WebSphere Application Server V8 Java 虛擬機中的 -Xgcpolicy:balanced 選項來實現。該技術旨在通過增量化整堆收集過程並將它合併到一種分代收集機制中,減少全局收集和壓縮所引起的非常長的暫停時間。儘管該技術具有一些不足,比如較長的平均暫停時間,但事實證明它在各種需要相對一致的暫停時間的部署場景中非常有用。由於技術上的進步,比如增量類卸載,根據具體需求,平衡收集器可能還適合其他部署。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章