簡介
Garbage collection (GC) 是 Java Virtual Machine (JVM) 的必要組成部分,它收集沒有使用的 Java 堆內存,以便應用程序可以繼續分配新的對象。GC 的效果和性能對於應用程序性能和確定 (determinism) 非常重要。IBM WebSphere Application Server V8 附帶的 IBM JVM(在受支持的平臺上)提供 4 種 GC 策略算法:
-Xgcpolicy:optthruput
-Xgcpolicy:optavgpause
-Xgcpolicy:gencon
-Xgcpolicy:balanced
每種算法都提供不同的性能和決定質量。此外,WebSphere Application Server V8 中的默認策略已從 -Xgcpolicy:optthruput
更改爲-Xgcpolicy:gencon
策略。下面我們逐一檢查這些策略,看看這個默認策略更改對它們有何影響。
垃圾收集器
不同應用程序自然有不同的內存使用模式。計算密集型數字處理工作負載使用 Java 堆 (heap) 的方式不同於面向客戶的高度事務型接口。要以最佳方式處理這些不同種類的工作負載,則需要使用不同的垃圾收集策略。IBM JVM 支持許多垃圾收集策略,它允許您選擇最適合您的應用程序的策略。
平行式 “標記-清掃-壓縮” 收集器:optthruput
最簡單的垃圾收集技術可能是:持續分配直到耗盡閒置內存,然後停止應用程序,處理整個堆。儘管這種技術可能會生成一個非常有效的垃圾收集器,但這意味着用戶程序必須能容忍收集器帶來的暫停。只關注總流量的工作負載可能會從這種策略中受益。
optthruput 策略 (-Xgcpolicy:optthruput
) 採用的就是這種策略(參見圖 1)。這個收集器)使用一種平行的 “標記-清掃 (mark-sweep)” 算法。簡言之,這意味着收集器首先逐一訪問可訪問的對象,將它們標記爲實時數據。然後,第二輪訪問掃除未標記的對象,將未使用的閒置內存留作新分配之用。大部分這種工作都可以並行完成,因此收集器可以使用額外的線程(默認情況下使用的最大線程數爲
CPU 的數量)來加快工作速度,減少應用程序的暫停時間。
圖 1. 應用程序和收集器 CPU 使用情況:optthruput
“標記-清掃” 算法的問題是可能會導致碎片(fragmentation),如圖 2 所示。儘管可能有大量閒置內存,但如果它們只是一些小塊,其間夾雜着活動對象,那麼可能沒有哪個碎塊大到足以滿足某個特定分配需求。
這個問題的解決方法是壓縮(compaction)。理論上,壓縮程序會將所有活動對象都移動到堆的一端,留下一塊連續的閒置空間。這是一項昂貴的操作,因爲可能會移動每個活動對象,每個經過移動的對象的指針都必須更新爲新位置。因此,通常只在萬不得已時才進行壓縮。壓縮也可以並行執行,但這會降低活動對象的打包效果,可能會生成幾個較小的閒置空間,而不是一整塊閒置空間。
圖 2. 堆碎片
併發收集器:optavgpause
對於願意損失部分流量、減少暫停時間的應用程序而言,可以選擇另一種策略。optavgpause 策略(-Xgcpolicy:optavgpause
)試圖在停止應用程序之前儘可能多完成一些 GC 工作,從而縮短暫停時間(參見圖 3)。這種策略也使用 “標記-清掃-壓縮 (mark-sweep-compact)” 收集器,但大部分標記和清掃工作可以在應用程序運行時執行。根據程序的分配速度,系統試圖預測下次需要執行垃圾收集的時間。達到這個閾值時,就會啓動一個併發
GC。當應用程序線程分配對象時,系統偶爾會要求它們在完成分配工作之前執行少量 GC 工作。線程執行的分配工作越多,要求它完成的 GC 工作也越多。與此同時,會有一個或多個背景 GC 線程使用閒置週期完成餘下的工作。如果已經完成所有併發工作,或者閒置內存提前耗盡,則將中止應用程序並完成收集工作。這種暫停通常比較短,除非需要進行壓縮。由於壓縮需要移動和更新活動對象,因此不能併發執行。
圖 3. 應用程序和收集器 CPU 使用情況:optavgpause
分代收集器:gencon
很久以前,人們就注意到,創建的大多數對象只被使用一小段時間。這是編程技術和應用程序類型所導致的結果。許多常用 Java 慣用語都會創建一些將迅速棄用的幫助程序 (helper) 對象,比如StringBuffer/StringBuilder
對象和 Iterator 對象。可以分配這些對象來完成某個特定任務,任務完成後就很少會再用到這些對象。在更大的範圍內,實際上事務型應用程序也常常創建一些 “一次性使用、用完作廢” 的對象組。一旦返回數據庫查詢的響應之後,就不再需要回復、中間狀態和查詢本身。
這種發現導致了分代(generational)垃圾收集器的開發。其背後的理念是:將堆分割爲多個不同區域,以不同的速度收集這些區域。新對象被分配到一個稱爲託兒所(nursery)(或新空間)的區域中。由於這個區域中的大多數對象很快都將變爲垃圾,所以收集該區域最有利於恢復內存。如果某個對象可能會存活一段時間,則會將它移動到另一個稱爲保留區 (tenure)(或舊空間)的區域中。這些對象不太可能變爲垃圾,因此收集器很少檢查它們。對於適當的工作負載,進行垃圾收集的結果是:由於檢查的內存更少,收集更快更有效;而且,經過檢查的對象被回收的比例更高一些。收集更快意味着暫停時間更短,因此應用程序響應性也更好。
IBM 的 gencon 策略(-Xgcpolicy:gencon
)在上述併發策略之上提供了一個分代 GC( “gen-”)。保留區空間如上所述收集,而託兒所空間使用了一個複製 (copying) 收集器。這種算法的工作方式是將託兒所區域進一步細分爲分配 (allocate) 和倖存者 (survivor) 空間(參見圖 4)。新對象被放置到分配空間中,直到耗盡其閒置空間。然後,應用程序會停止,分配空間中的所有活動對象都將複製到倖存者空間中。然後這兩個空間交換角色:分配空間變爲倖存者空間,倖存者空間變爲分配空間,應用程序恢復運行。如果某個對象在幾輪複製之後得以倖存,則會將它移動到保留區空間中。
圖 4. gencon 應用
理論上,這意味着託兒所空間的一半(即倖存者空間)在任何時點上都未使用。實際上,預留爲倖存者空間的內存量會根據在每次收集中倖存下來的對象的百分比進行實時調整。如果大多數新對象都被收集(這是預期的情況),那麼分配空間和倖存者空間之間的分界線就會傾斜,此時需要增加垃圾收集之前可以分配的數據量。
這種風格的收集器有一個重大好處:通過在每次收集時移動活動對象,託兒所區域在每次收集時都被隱式壓縮。這會導致閒置空間塊變得儘可能的大,但也可能會將關係密切的對象(例如String
及其
char[]
數據)移動到臨近的內存位置。這有助於改進系統內存緩存的性能特徵,從而提高應用程序本身的性能。
託兒所垃圾收集的成本與倖存的數據量有關(參見圖 5)。由於預期的情況是多數對象都將是垃圾,因此一次託兒所收集通常導致很短的暫停。儘管應該能夠快速收集多數對象,但有些對象無法收集。這意味着隨着時間的推移,保留區區域中將塞滿了長期存活的對象,最終導致需要對整個堆進行一次垃圾收集。上述併發收集器使用的大部分技術在這裏仍然適用。保留區區域的標記將根據需要併發運行,而分配和收集是在託兒所區域中進行的。在 gencon 策略下,保留區區域的清掃不是併發執行的,而是作爲保留區主收集的一部分進行的。
圖 5. 應用程序和收集器 CPU 使用情況:gencon
基於區域的收集器:balanced
WebSphere Application Server V8 中添加了一個新的垃圾收集策略。這個策略名爲 balanced(-Xgcpolicy:balanced
),它擴展了擁有多個不同的堆區域這個概念,將堆劃分爲大量區域,每個區域都可以單獨處理。本系列第 2 部分將詳細介紹基於區域的垃圾收集的基礎知識,特別是將深入討論 balanced 策略。
回頁首
調優非分代收集器堆設置
要調優任何應用程序的堆大小,第一步是使用默認堆設置運行應用程序,這允許您測量開箱即用性能。此時,如果堆閒置空間總是低於 40%,或者 GC 暫停高於總運行時間的 10%,就應該考慮增加堆大小。最小堆大小和最大堆大小可以分別通過-Xms<value>
和
-Xmx<value>
修改。
用於垃圾收集的標記和清掃階段的 GC 暫停時間基於堆上的活動對象的數量。當您增加統一工作負載上的堆大小時,標記和清掃階段將繼續花費大致相同的時間完成操作。因此,通過增加堆大小,可以增加 GC 暫停之間的間隔,從而爲應用程序提供更多的執行時間。
如果 GC 由於碎片問題而執行壓縮,那麼增加堆大小可能有助於緩解壓縮導致的長時暫停問題。壓縮階段可能會極大地增加 GC 暫停時間,因此,如果壓縮階段經常出現,那麼調優堆設置就能改進應用程序性能。
固定大小堆與可變大小堆
使用可變大小堆允許 GC 僅對堆使用應用程序必需的 OS 資源。隨着應用程序程序堆需求的變化,GC 可以通過擴大和收縮堆做出反應。GC 只能收縮從堆末尾開始的連續內存塊,因此收縮堆可能需要進行壓縮。實際的收縮和擴大階段很快就能完成,不會明顯增加 GC 暫停時間。通過將最大堆大小設置爲略大於常規操作所需的大小,應用程序能夠通過擴大堆來處理額外的工作負載。
堆需求不變的應用程序可以通過使用固定堆大小改進 GC 暫停時間。
回頁首
調優分代 GC
調優分代垃圾收集時,最簡單的方法是將託兒所空間視爲非分代垃圾收集使用的 Java 堆區域之外的新 Java 堆區域。這樣,非分代垃圾收集使用的 Java 堆就變成了保留區堆。
這種方法是一種保守方法:預期的情況是保留區堆的佔用率將由於託兒所空間的引入而降低,但它提供了一個安全的起點,特別是從非分代策略遷移時。當可以監控全局(完全)收集之後的保留區堆的佔用率時,就可以按照前面 描述的方法來調整堆大小:
-Xmn<size>
設置託兒所區域的初始和最大大小,有效地設置-Xmns
和-Xmnx
。-Xmns<size>
將託兒所區域的初始大小設置爲指定的值。-Xmnx<size>
將託兒所區域的最大大小設置爲指定的值。
託兒所堆大小應該是固定的,因此只需要這些選項中的一個:-Xmn
。因此,您只需理解如何正確設置託兒所堆大小。
設置託兒所堆大小
要正確設置託兒所堆大小,首先需要考慮託兒所收集使用的機制,然後考慮隨之出現的二級特徵:
- 託兒所收集的工作方式是將數據從分配空間複製到倖存者空間。複製數據是一個比較昂貴耗時的任務。因此,託兒所收集所花費的時間由需要複製的數據量決定。這不是說要複製的對象的數量與託兒所空間自身的大小沒有影響,而是說與複製實際數據的成本相比,這些因素造成的影響相對較小。因此,託兒所收集所花費的時間與需要複製的數據量成正比。
- 在任何給定的收集中,只有有限和固定的數據量是 “實時的”。一旦應用程序完成啓動並完全填充其緩存後,託兒所堆中需要複製的 “實時” 數據量就由該時點需要完成的工作量來確定。在處理事務的系統中,需要複製的實時數據量等於某個實時事務集。例如,如果您使用支持 50 個併發事務發生的 50 個 WebContainer 線程來配置您的應用服務器,那麼實時數據量就是與那 50 個事務關聯的數據量。
這意味着,託兒所收集所需的時間由收集時發生的併發事務的數量的關聯數據的大小決定,而不是由託兒所空間的大小決定。這還意味着,隨着託兒所空間的大小增大,託兒所收集之間的間隔時間會隨之增大,但收集所需的時間不會增加。事實上,隨着託兒所空間增大,垃圾收集所需的總時間會隨之降低。
圖 6 顯示,如果託兒所空間的大小低於事務集的關聯實時數據的大小,因此託兒所收集之間的時間間隔低於一個事務,則必須多次複製這些數據。
圖 6. 數據複製的平均次數與託兒所收集之間的時間間隔
隨着託兒所空間大小和託兒所收集之間的時間間隔增加,需要複製的數據量通常會隨之減少,垃圾收集的開銷也會隨之降低。
託兒所堆大小限制
IBM 垃圾收集器或 JVM 沒有對託兒所堆大小進行直接限制;事實上,託兒所堆大小有時被設置爲 10 GB 甚至 100 GB。但是,操作系統在 Java 進程使用的虛擬內存、進程地址空間以及足夠的物理內存(RAM)的可用性方面有一些限制。一個 32 位進程在每個平臺上的操作系統限制如圖 7 所示。
圖 7. 按操作系統列示的 32 位地址空間
對 64 位進程的限制要嚴格得多。由於可尋址內存的範圍從數百到數十億 GB,可用物理內存 (RAM) 的限制變得更加重要。
回頁首
將二者結合起來
如上所述,最簡單的方法是將託兒所空間視爲一個額外的內存空間。但是,託兒所堆和保留區堆實際上都被分配爲單個連續內存段,它們的大小可通過 -Xmx
設置進行控制。如果只使用-Xmx
設置,則
-Xmx
值的 25% 用於最大託兒所堆大小,託兒所堆大小允許在那 25% 之內進行伸縮。下面提供了 Java 堆佈局,如圖 8 所示。
圖 8. 默認堆佈局
但是,您應該將託兒所堆大小固定爲一個較大的值,以最小化垃圾收集花費的時間,增強保留區堆,使其根據佔用率重置自身大小,從而提高彈性。因此,首選的 Java 堆佈局如圖 9 所示。
圖 9. 推薦的堆佈局
要實現這個佈局,託兒所空間和保留區空間的最小和最大堆大小的值應該設置如下:各個託兒所空間大小的最大和最小值都相等,而各個保留區空間大小的最小和最大值各不相同。
例如,如果您想擁有一個 256MB 的託兒所堆大小,而保留區堆大小介於 756MB 和 1024MB 之間,則這些值應該爲:
-Xmns256M
-Xmnx256M
-Xmos756M
-Xmox1024M
回頁首
遷移到分代策略
由於 WebSphere Application Server V8 中的默認 GC 策略已經從 optthruput 變爲 gencon,因此需要調整以前選擇的調優參數。主要問題是更改堆大小來補償託兒所空間。以前在 optthruput 策略中,以 1G 堆大小(即-Xmx1G
)運行正常的程序在使用 768M 保留區空間和 256M 託兒所空間運行時可能會出現問題。前面介紹的技術將有助於您選擇新的堆參數。
還有一些不太明顯的情況,gencon 可能會表現出不同的行爲。
由於類通常是長期存活的對象,因此可以將它們直接分配到保留區空間。因此,類卸載只能是保留區收集的一部分。如果應用程序非常依賴短期存活的類加載器,且託兒所收集能夠及時處理其他已分配對象,那麼保留區收集可能不會頻繁發生。這意味着,類和類加載器的數量將持續增長,這可能會增加本機內存上的壓力,而且會在需要進行保留區收集時導致收集時間過長,這是因爲有太多的類卸載工作需要完成。
如果出現這個問題,有兩種解決方法。第一種方法是在出現大量類加載器時鼓勵進行額外的保留區收集。命令行選項 -Xgc:classUnloadingKickoffThreshold=<number>
告知系統,每當新創建的類加載器達到<number>
時,就會啓動一次併發保留區收集。因此,如果指定
-Xgc:classUnloadingKickoffThreshold=100
,那麼託兒所收集會仔細觀察,每當自上次保留區收集以來新創建的類加載器數量達到 100 時,就會啓動一次併發保留區收集。第二種方法是改爲使用另一種 GC 策略。
類似的問題可能會出現在引用對象(例如 java.lang.ref.Reference
的子類)和使用 finalize()
方法的對象上。如果這兩種對象存活的時間足夠長,在變得無法訪問之前被移動到保留區空間中,那麼可能會經過很長時間以後纔會運行保留區收集,“發現” 這個對象已經死亡。如果這些對象佔用着大型或稀有本機資源,就有可能導致出現問題。我們將這種對象戲稱爲 “冰山” 對象:表面上它們只佔用很小的 Java 堆大小,但下面隱藏着巨大的本機資源,垃圾收集器看不到。就像面對真正的冰山一樣,最好的策略是儘可能遠離它們。即使使用其他
GC 策略,也無法保證能夠探測到可終結的對象,並及時運行它的終止程序 (finalizer)。如果它們佔用稀有資源,儘可能地手動釋放它們可能是最佳策略。
更改策略
默認策略應該能爲多數工作負載提供足夠的性能,但它可能不是某個特殊應用程序的理想選擇。
類似 “批作業” 的應用程序初始化狀態並加載要處理的數據。這些對象中的大部分將在作業期間存活,只有少數幾個額外對象是在作業運行時創建的。這種工作負載適合 optthruput 模式,因爲預期的情況是:在任務完成之前,幾乎沒有垃圾。另一種類似情況是,如果作業很快完成或只分配很少的對象,那麼只要堆大小適當,作業運行就不需要垃圾收集。在上述這些情況下,optthruput 收集器的最小開銷會讓您做出最佳選擇。
相比之下,事務型應用程序不斷創建並啓用對象組。在這個上下文中,術語 “事務” 主要使用字面意義(比如數據庫更新或電子商務採購)或者採用更寬泛的意義,比如一項獨立工作。舉例來說,服務一個 Web 頁可以視爲一個事務。客戶機提交一個 URL,服務器計算頁面內容並將其發送給客戶機。一旦客戶機接收到頁面,服務器就可以丟棄已計算的數據。
爲了進一步闡述這個定義,我們來看一個標準用戶界面。用戶單擊 Save 按鈕之後,系統會打開一個文件對話框,以便用戶導航文件系統,爲文檔選擇一個位置。一旦用戶關閉對話框,就不再需要所有中間狀態。實質上,有些批作業甚至也是事務型的。假設一個任務正在爲一些大型圖像文件創建縮略圖,那麼該作業看起來似乎是一個大型批作業,但在內部,作業分別處理每個圖像,每個處理都形成一個 “事務”。對於這類工作負載,gencon 模式應該能提供好處。
optavgpause 模式介於二者之間。這種模式適用的應用程序的特點是:擁有大量長期存活的數據,這些數據隨着程序運行緩慢更改。對於工作負載而言,這種模式比較少見。通常,長期存活的數據要麼從不更改,要麼頻繁更改。也就是說,如果系統擁有緩慢演變的數據,且這些數據創建的中間對象也不多,那麼這種系統就適合使用這個策略。由於前面討論的某種原因而不能在 gencon 策略下有效運行的程序可能會受益於 optavgpause 的併發性。
回頁首
結束語
本文簡要描述了 WebSphere Application Server V8 中的 Java Virtual Machine 中提供的垃圾收集策略。儘管默認設置應該適用於多數情況,但是,爲了獲得最佳性能,可能需要進行一些調優。通過匹配工作負載類型及其使用的 GC 策略並選擇適當的堆參數,應該能夠減少垃圾收集對應用程序的影響。
本系列第 2 部分將介紹一種基於區域的新的垃圾收集策略 balanced,該策略旨在在 64 位大型多核系統上部署時提高可伸縮性。下一篇文章會涉及到這種新技術背後的動機、它提供的性能改進以及關於調優這個新選項的提示和技巧。