可以使用 4 種不同的策略配置 IBM Developer Kit for the Java 5.0 Platform(IBM SDK)中的垃圾收集(GC)。本文(關于 GC 的兩篇文章的第一篇)介紹不同的垃圾收集策略并討論它們的性質。在閱讀本文之前,您應該對 Java 平臺中的垃圾收集有基本的認識。第 2 部分將給出一種選擇策略的量化方法,以及一些示例。
為什么要有不同的 GC 策略?
能夠使用不同的策略使開發人員增加了對應用程序的控制能力。有許多種 GC 算法,每種算法各有優缺點,這取決于工作負載的類型。(如果您不熟悉 GC 算法的一般性主題,那么請參見 參考資料 中其他讀物的鏈接。在 IBM SDK 5.0 中,可以用 4 種策略 之一配置垃圾收集,每種策略都使用自己的算法。默認策略對于大多數應用程序已經足夠了。如果對應用程序的性能沒有特別的要求,那么您對本文(和下一篇文章)的內容可能不感興趣;可以在不改變 GC 策略的情況下運行 IBM SDK 5.0。但是,如果應用程序需要最優的性能,或者很關注 GC 停頓時間的長度,那么請讀下去。您會看到最新的版本比以前的版本提供了更多選擇。
那么,為什么不讓 Java 運行時的 IBM 實現自動地替您做出選擇呢?因為這不總是可行的。運行時很難了解您的需要。在某些情況下,希望應用程序有很高的吞吐量;而在其他情況下,希望減少停頓時間。
表 1 列出可用的策略并解釋每種策略應該在何時使用。后面幾節分別詳細描述每種策略的性質。
策略 | 選項 | 描述 |
---|---|---|
針對吞吐量進行優化 | -Xgcpolicy:optthruput (可選) |
默認策略。對于吞吐量比短暫的 GC 停頓更重要的應用程序,通常使用這種策略。每當進行垃圾收集時,應用程序都會停頓。 |
針對停頓時間進行優化 | -Xgcpolicy:optavgpause |
通過并發地執行一部分垃圾收集,在高吞吐量和短 GC 停頓之間進行折中。應用程序停頓的時間更短。 |
分代并發 | -Xgcpolicy:gencon |
以不同方式處理短期存活的對象和長期存活的對象。采用這種策略時,具有許多短期存活對象的應用程序會表現出更短的停頓時間,同時仍然產生很好的吞吐量。 |
子池 | -Xgcpolicy:subpool |
采用與默認策略相似的算法,但是采用一種比較適合多處理器計算機的分配策略。建議對于有 16 個或更多處理器的 SMP 計算機使用這種策略。這種策略只能在 IBM pSeries® 和 zSeries® 平臺上使用。需要擴展到大型計算機上的應用程序可以從這種策略中受益。 |
![]() |
|
在本文中,用表 1 中命令行選項中的縮寫來表示這些策略:optthruput
表示針對吞吐量進行優化,optavgpause
表示針對停頓時間進行優化,gencon
表示分代并發,subpool
表示子池。
何時應該考慮采用非默認的 GC 策略?
建議您總是先使用默認 GC 策略。 在放棄默認策略之前,需要了解在哪些情況下應該采用其他策略。表 2 給出了一些原因:
切換到 | 原因 |
---|---|
optavgpause |
|
gencon |
|
subpool |
|
我要強調一點:即使出現了表 2 中提到的原因,也不足以 斷言替代策略的性能會更好;它們只是提示。在所有情況下,都應該實際運行應用程序,并度量吞吐量和/或響應時間以及 GC 停頓時間。本系列的下一部分將給出進行這種測試的示例。
本文余下的幾節詳細描述 GC 策略之間的差異。
![]() ![]() |
![]()
|
optthruput
![]() |
|
optthruput
是默認策略。它是一個追蹤收集器,稱為標志-掃描-緊湊排列(mark-sweep-compact) 收集器。在 GC 期間總是會運行標志和掃描階段,但是緊湊排列只在某些情況下發生。標志階段會尋找所有存活的對象并加上標志。掃描階段會刪除所有未加標志的對象。第三個可選的步驟是緊湊排列(compaction)。在某些情況下可能會發生緊湊排列;最常見的情況是系統無法回收足夠的空閑空間。
如果非常頻繁地分配和釋放對象,導致在堆上只留下小塊的空閑內存,這時就出現了碎片化。整個堆上可能有大量的空閑空間,但是連續區域很小,導致分配失敗。緊湊排列 就是將所有對象向下移動到堆的開頭,一個挨一個地排列,讓它們之間沒有間隔空間。這會消除堆的碎片化,但這是一種代價昂貴的任務,所以只在必要時執行。
圖 1 描述三個不同階段之后的堆布局:標志、掃描和緊湊排列。深色區域表示對象,淺色區域表示空閑空間。
![]() |
|
不同 GC 階段的工作細節超出了本文的范圍;我主要關注確保您理解運行時性質。關于更多細節,請閱讀 Diagnostics Guide(參見 參考資料)。
圖 2 展示執行時間在應用程序線程(即 mutator)和 GC 線程之間如何分布。水平軸是經歷的時間,垂直軸包含線程,其中 n 表示計算機上處理器的數量。對于這個圖示,假設應用程序在每個處理器上使用一個線程。GC 由藍色框表示,這說明 mutator 停止,GC 線程正在運行。這些收集線程占用 100% 的 CPU 資源,mutator 線程空閑。這個圖有點兒過分籠統了,這是為了便于與本文中的其他策略進行比較。實際上,GC 的持續時間和頻率依賴于應用程序和工作負載。
![]() |
|
堆鎖和線程分配緩存
optthruput
策略使用連續的堆區域,應用程序中的所有線程共享這個區域。線程需要排他地訪問堆,以便為新對象保留空間。這個鎖稱為堆鎖(heap lock),它們確保任意時刻只有一個線程能夠分配對象。在有多個 CPU 的計算機上,這個鎖會造成伸縮性問題,因為可能同時出現多個分配請求,但是每個請求需要排他地訪問堆鎖。
為了緩解這個問題,每個線程保留一小塊內存,稱為線程分配緩存(thread allocation cache) (也稱為線程局部堆,TLH)。這塊存儲空間是一個線程專用的,所以在其中進行分配時不使用堆鎖。當分配緩存滿了之后,線程使用堆鎖向堆請求新的分配緩存。
堆的碎片化會妨礙線程獲得較大的 TLH,所以 TLH 會很快被填滿,導致應用程序線程頻繁地向堆請求新的分配緩存。在這種情況下,堆鎖就成了瓶頸;如果出現這樣的情況,gencon
或 subpool
策略可能是比較好的替代方案。
![]() ![]() |
![]()
|
optavgpause
對于許多應用程序,吞吐量不如響應時間那么重要。假設一個應用程序要求在 100 毫秒內完成對工作項目的處理。如果 GC 停頓時間在 100 毫秒級別,那么在 GC 期間就無法在規定時間內完成處理。垃圾收集的一個問題是,停頓時間會增加處理項目花費的最大時間。大型堆(在 64 位平臺上可用)會加劇這種影響,因為垃圾收集要處理更多的對象。
![]() |
|
optavgpause
是一個替代的 GC 策略,其設計目的是使停頓時間最小化。它并不保證特定的停頓時間,但是停頓時間會比默認 GC 策略產生的停頓時間短。它采用的思路是在應用程序運行的同時并發地執行一些垃圾收集工作。這通過兩種手段來實現:
根據應用程序的不同,與默認 GC 策略相比,吞吐量性能會有 5% 到 10% 的下降。
圖 3 展示在使用 optavgpause
策略時執行時間在 GC 線程和 mutator 線程之間如何分布。沒有顯示后臺追蹤線程,因為它應該不會影響應用程序的性能。
圖中的灰色區域表示啟用了并發追蹤,每個 mutator 線程必須放棄它的一部分處理時間。每個并發階段之后進行一次完整的垃圾收集,垃圾收集完成在并發階段沒有完成的標志和掃描工作。由此導致的停頓時間應該會比一般 GC(optthruput
)短得多,這在圖 3 中表現為 GC 框的時間跨度更小。從 GC 結束到并發階段開始之間的間隔是變化的,但是這個階段對性能沒有顯著影響。
![]() ![]() |
![]()
|
gencon
分代的垃圾收集策略考慮到了對象的生命期,并將對象放在堆的不同區域。如果應用程序中的大多數對象在年輕時就死了 (也就是說,它們不會在許多次垃圾收集中存活下來),那么使用單一的堆就會影響性能;分代的垃圾收集方式就是試圖解決這個問題。
利用分代的 GC,以不同方式對待長期存活的對象和短期存活的對象。堆分割為嬰兒 區域和 長存(tenured) 區域,見圖 4。在嬰兒區域中創建對象,如果它們存活的時間足夠長,就會轉移到長存區域。對象在經歷了一定次數的垃圾收集之后,如果仍然存活,就會被轉移。其思路是大多數對象是短期存活的;通過頻繁地收集嬰兒區域中的對象,這些對象就可以被釋放,而不需要負擔收集整個堆的成本。對長存區域的垃圾收集不會頻繁進行。
正如在圖 4 中看到的,嬰兒區域本身又分成兩個區域:分配(allocate) 和幸存(survivor)。對象在分配區域中進行分配,當分配區域填滿時,存活的對象被復制到幸存空間或長存空間(這取決于它們的 “年齡”)。然后,嬰兒區域中的空間交換用途,分配變成幸存,幸存變成分配。由已死亡的對象占用的空間可以被新分配覆蓋。嬰兒收集稱為清掃(scavenge);圖 5 說明在這個過程中發生了什么:
當分配空間滿了時,觸發垃圾收集。然后,追蹤存活的對象并將它們復制到幸存空間。如果大多數對象已經死亡了,那么這個過程實際上成本很低。另外,已經達到復制次數閾值的對象會轉移到長存空間。此后,這個對象就被稱為長存的。
正如分代并發 這個術語所暗示的,gencon
策略有并發成分。長存空間采用一種與 optavgpause
策略相似的方式進行并發標志,但是沒有并發掃描。在并發階段,所有分配都會導致輕微的吞吐量損失。采用這種方式時,由于長存空間收集導致的停頓時間會比較短。
圖 6 顯示在運行 gencon
GC 時執行時間的分布:
清掃的時間很短(由紅色的小框表示)?;疑硎鹃_始并發追蹤(以后是收集長存空間),其中的一些操作是并發執行的。這稱為全局收集(global collection),它包括一次清掃和一次長存空間收集。進行全局收集的頻繁程度取決于堆的大小和對象的生命期。長存空間收集應該相當快,因為它的大部分已經并發地收集了。
![]() ![]() |
![]()
|
subpool
subpool
策略可以幫助在多處理器系統上提高性能。正如前面提到的,只能在 IBM pSeries 和 zSeries 計算機上使用這種策略。堆布局與 optthruput
策略相同,但是空閑列表的結構不一樣。不是為整個堆使用一個空閑列表,而是有多個列表,稱為子池(subpool)。每個池按照大小進行排序。特定大小的分配請求可以由此大小的池快速地滿足。使用原子性(與平臺相關的)高性能指令將空閑列表項彈出這個列表,避免了串行訪問。圖 7 展示了如何按照大小組織空閑存儲塊:
當 JVM 啟動時或進行了緊湊排列時,不使用子池,因為有大塊的堆空間空閑著。在這些情況下,每個處理器用自己專用的小型堆來滿足請求。當發生第一次垃圾收集時,掃描階段開始填充子池,后續的分配主要使用子池。
subpool
策略可以減少分配對象花費的時間。原子性指令確保在不需要全局堆鎖的情況下執行分配。處理器局部的小型堆會提高效率,因為減少了緩存沖突。這會直接影響可伸縮性,尤其是在多處理器系統上。在不能使用 subpool
的平臺上,分代的 GC 可以提供相似的好處。
![]() ![]() |
![]()
|
結束語
本文描述了 IBM SDK 5.0 中的不同 GC 策略以及它們的一些性質。默認策略對于大多數應用程序是足夠的;但是,在某些情況下,其他策略的性能更好。我介紹了應該考慮切換到 optavgpause
、gencon
或 subpool
的一些一般場景。在對策略進行評估時,對應用程序性能進行度量是非常重要的,第 2 部分將詳細演示這個評估過程。
原文轉自:http://www.anti-gravitydesign.com