 |
級別: 初級
Sumit Chawla, 技術主管,eServer Java Enablement, IBM
2003 年 1 月 01 日
您的 Java 應用程序充分利用了所運行的 IBM eServer 硬件的能力了嗎?在本文中,作者將介紹如何判斷垃圾收集 —— Java 虛擬機執行的收回不再使用空間的后臺任務 —— 是否調節到最佳狀態。然后,他將提供一些解決垃圾收集問題的建議。
簡介
垃圾收集實現是 IBM Java Virtual Machine(JVM)卓越性能的關鍵。其他多數 JVM 都需要大量調整才能提供最優性能,而 IBM JVM 利用其“開箱即用”的默認設置,在多數情況下都能工作得很好。但是在某些情況下,垃圾收集的性能會莫名其妙地迅速降低。結果可能導致服務器沒有響應、屏幕靜止不動或者完全失效,并常常伴有“堆空間嚴重不足”這類含糊的消息。幸運的是,多數情況下都很容易找到原因,通常也很容易糾正錯誤。
本文將介紹如何確定造成性能下降的潛在原因。因為垃圾收集是一個很大、很復雜的話題,所以本文是在已經發表的一組相關文章的基礎上展開討論(請參閱參考資料)。雖然本文討論的多數建議都將 Java 程序看作是一個黑盒子,但仍然有一些觀點可以在設計或編碼時運用,以避免潛在的問題。
本文提供的信息適用于所有 IBM eServer 平臺,除了可重新設置的 JVM 配置(請參閱參考資料)。除非特別說明,文中的例子都取自運行在四處理器 AIX 5.1 上的 Java 1.3.1 build ca131-20020706。
堆管理概述
JVM 在初始化的過程中分配堆。堆的大小取決于指定或者默認的最小和最大值以及堆的使用情況。分配堆可能對可視化堆有所幫助,如圖 1 所示,其中顯示了 heapbase、heaplimit 和 heaptop。 圖 1. 堆的概念視圖
Heapbase 表示堆底,heaptop 則表示堆能夠增長到的最大絕對值。差值(heaptop - heapbase )由命令行參數 -Xmx 決定。該參數和其他命令行參數都是在關于 verbosegc 和命令行參數的 developerWorks 文檔中描述的。heaplimit 指針可以隨著堆的擴展上升,隨著堆的收縮下降。heaplimit 永遠不能超過 heaptop,也不能低于使用 -Xms 指定的初始堆大小。任何時候堆的大小都是 heaplimit - heapbase 。
如果整個堆的自由空間比例低于 -Xminf 指定的值(minf 是最小自由空間),堆就會擴展。如果整個堆的自由空間比例高于 -Xmaxf 指定的值(maxf 是最大自由空間),堆就會收縮。-Xminf 和 -Xmaxf 的默認值分別是 0.3 和 0.6,因此 JVM 總是嘗試將堆的自由空間比例維持在 30% 到 60% 之間。參數 -Xmine (mine 是最小擴展大小)和 -Xmaxe (maxe 是最大擴展大小)控制擴展的增量。這 4 個參數對固定大小的堆不起作用(用相等的 -Xms 和 -Xmx 值啟動 JVM,這意味著 HeapLimit = HeapTop ),因為固定大小的堆不能擴展或收縮。
當 Java 線程請求存儲時,如果 JVM 不能分配足夠的存儲塊來滿足這個請求,則可以說出現了分配失敗(AF)。這時候就不可避免要進行垃圾收集。垃圾收集包括收集所有“不可達的(unreachable)”引用,以便重用它們所占用的空間。垃圾收集由請求分配的線程執行,是一種 Stop-The-World(STW)機制;執行垃圾收集算法時,Java 應用程序的其他所有線程(除了垃圾收集幫助器線程之外)都被掛起。
IBM 實現使用稱為 mark-sweep-compact(MSC)的垃圾收集算法,它是根據三個不同的階段命名的。
- 標記
- 表和所有“可達的”或者活動的對象。這個階段從確定“根”開始,比如線程棧上的對象、Java Native Interface(JNI)局部引用和全局引用等,然后沿著每個引用進行遞歸,直到所有的引用都做上標記。
- 清理
- 清除所有已經分配但沒有標記的對象,收回這些對象使用的空間。
- 壓縮
- 將活動對象移動到一起,去掉堆中的空洞(hole)和碎片。
關于 MSC 算法的細節,請參閱參考資料。
并行和并發
雖然垃圾收集本身采用 STW 機制,但最新的 IBM JVM 版本都在多處理器機器上使用多個“幫助器”線程,以便減少每個階段所花費的時間。因此,在默認情況下,JVM 1.3.0 在標記階段采用并行模式。JVM 1.3.1 在標記和清理階段都采用并行模式,在標記階段還支持一種可選的并發模式,可以使用命令行開關 -Xgcpolicy:optavgpause 切換。撰寫本文時,最新版本的 JVM 1.4.0 中有一種遞增壓縮模式,它并行化了(parallelize)壓縮階段。討論這些模式時,重要的是要理解并行和并發的區別。
在擁有 N 個 CPU 的多處理器系統中,支持并行模式的 JVM 在初始化的時候會啟動 N-1 個垃圾收集幫助器線程。運行應用程序代碼的時候,這些線程一直處于空閑狀態,只有當啟動垃圾收集時才調用它們。在某一特定的階段,會將一些工作分配給驅動垃圾收集的線程和幫助器線程,因此一共有 N 線程并行地運行在 N -CPU 機器上。如果要禁止并行模式,惟一的方法是使用 -Xgcthreads 參數改變啟動的垃圾收集幫助器線程個數。
采用并發模式時,JVM 會啟動一個后臺線程(不利于垃圾收集幫助器線程),在執行應用程序線程的同時,部分工作是在后臺完成的。后臺線程會試著與應用程序并發執行,以完成垃圾收集的所有操作,因此在執行垃圾收集時,可以減少 STW 造成的暫停。但在某些情況下,并發處理可能對性能造成負面影響,特別是對于 CPU 敏感的應用程序。
下表按照 JVM 版本列出了垃圾收集各個階段的處理類型。
|
標記 |
清理 |
壓縮 |
IBM JVM 1.2.2 |
X |
X |
X |
IBM JVM 1.3.0 |
P |
X |
X |
IBM JVM 1.3.1 |
P, C |
P |
X |
IBM JVM 1.4.0 |
P, C |
P |
P |
其中:
- X
- 單線程操作。
- P
- 并行操作(垃圾收集期間所有幫助器線程都在工作)。
- C
- 并發操作(后臺線程和應用程序線程并發操作)。
分析 verbosegc 輸出
雖然也有分析程序和其他第三方工具,但本文僅討論對 verbosegc 日志的分析。這些日志由 JVM 在指定 -verbosegc 命令行參數時生成,是一種非常可靠的獨立于平臺的調試工具。要獲得完整的 verbosegc 語法,請參閱“verbosegc and command-line parameters”。
啟用 verbosegc 可能對應用程序的性能有一定影響。如果這種影響是無法接受的,則應該使用測試系統來收集 verbosegc 日志。服務器應用程序通常一直使 verbosegc 處于激活狀態。這是監控整個 JVM 是否運轉良好的一種好辦法,在出現 OutOfMemory 錯誤的情況下,這種方法具有無可估計的價值。
為了有效地分析 verbosegc 記錄,必須把精力集中在相關信息上,并過濾掉“噪音”。通過編寫腳本從很長的 verbosegc 追蹤記錄中提取信息并不難,但是這些記錄的格式可能(而且通常確實如此)隨不同的 JVM 版本而異。下面的例子用粗體或藍色字體表示重要的信息。即使記錄的格式看起來相差很大,也很容易在 verbosegc 日志中找到這些信息。
您刷新的了嗎?
在嘗試本文中的建議之前,強烈建議您升級到最新的 JVM 服務刷新(SR)。每次進行新的服務刷新都會有很多修正和改進,應用新的服務刷新可以提高 JVM 的性能和穩定性。遷移到最新的版本(比如 JVM 1.4.0 或 1.3.1,根據使用的平臺)提供了增強的性能特性。一定要為 JVM 安裝所有必需的 OS 補丁(比如 AIX 上的維護級別)。這些信息記錄在隨 SDK/JRE 提供的 readme 文件中。
正確設置堆的大小
計算正確的堆大小參數很容易,但它可能對應用程序啟動時間和運行時性能有很大的影響。初始大小和最大值分別由參數 -Xms 和 -Xmx 控制,這些值通常是根據理想情況和重負荷情況下堆的使用情況的估計來設置的,但 verbosegc 可以幫助確定這些值,而避免胡亂猜測。下面是從啟動到完成程序的初始化(或者進入“就緒”狀態)這段時間里,一個應用程序的 verbosegc 輸出,如下所示。
<GC[0]: Expanded System Heap by 65536 bytes
<GC[0]: Expanded System Heap by 65536 bytes
<AF[1]: Allocation Failure. need 64 bytes, 0 ms since last AF>
<AF[1]: managing allocation failure, action=1 (0/3983128) (209640/209640)>
<GC(1): GC cycle started Tue Oct 29 11:05:04 2002
<GC(1): freed 1244912 bytes, 34% free (1454552/4192768), in 10 ms>
<GC(1): mark: 9 ms, sweep: 1 ms, compact: 0 ms>
<GC(1): refs: soft 0 (age >= 32), weak 5, final 237, phantom 0>
<AF[1]: completed in 12 ms>
|
上述記錄表明,第一次發生 AF 時,堆中的自由空間為 0%(3983128 中有 0 字節可用)。此外,第一次垃圾收集之后,自由空間比例上升到 34%,略高于 -Xminf 標記(默認為 30%)。根據應用程序的使用,使用 -Xms 分配更大的初始堆可能會更好一些。幾乎可以肯定的是,上例中的應用程序在下一次 AF 時會導致堆擴展。分配更大的初始堆可以避免這種情況。一旦應用程序進入 Ready 狀態,通常不會再遇到 AF,因此也就確定了比較好的初始堆大小。類似地,通過增加應用程序負載也可以探測到避免出現 OutOfMemory 錯誤的 -Xmx 值。
如果堆太小,即使應用程序不會長期使用很多對象,也會頻繁地進行垃圾收集。因此,自然會出現使用很大的堆的傾向。但是由于平臺和其他方面的因素,堆的最大大小還受物理因素的限制。如果堆被分頁,性能就會急劇惡化,因此堆的大小一定不能超出安裝在系統上的物理內存總量。比如,如果 AIX 機器上有 1 GB 的內存,就不應該為 Java 應用程序分配 2 GB 的堆。
即使應用程序在擁有 64 GB 內存的 p690 超級計算機上運行,也不一定就能使用 -Xmx60g (當然是 64 位的 JVM)。雖然在很長時間內,應用程序可能不會遇到 AF,但一旦發生 AF,STW 造成的停頓將很難應付。下面的記錄取自 32 GB AIX 系統上分配了 20 GB 堆空間的 64 位 JVM 1.3.1(build caix64131-20021102 ),它展示了大型堆在這方面的影響。
<AF[29]: Allocation Failure. need 2321688 bytes, 88925 ms since last AF>
<AF[29]: managing allocation failure, action=1 (3235443800/20968372736)
(3145728/3145728)>
<GC(29): GC cycle started Mon Nov 4 14:46:20 2002
<GC(29): freed 8838057824 bytes, 57% free (12076647352/20971518464),
in 4749 ms>
<GC(29): mark: 4240 ms, sweep: 509 ms, compact: 0 ms>
<GC(29): refs: soft 0 (age >= 32), weak 0, final 1, phantom 0>
<AF[29]: completed in 4763 ms>
|
垃圾收集用了將近五秒鐘,還不包括壓縮!垃圾收集周期所花費的時間直接與堆的大小成正比。一條好的原則是根據需要設置堆的大小,而不是將它配置得太大或太小。
常見的一種性能優化技術是將初始堆大小(-Xms )設成與最大堆大小(-Xmx )相同。因為不會出現堆擴展和堆收縮,所以在某些情況下,這樣做可以顯著地改善性能。通常,只有需要處理大量分配請求的應用程序時,才在初始和最大堆大小之間設置較大的差值。但是要記住,如果指定 -Xms100m -Xmx100m ,那么 JVM 將在整個生命期中消耗 100 MB 的內存,即使利用率不超過 10%。
另一方面,也可以使用 -Xinitsh 在開始的時候分配較大的系統堆,從而避免出現 Expanded System Heap 消息。但這些消息完全可以忽略。系統堆隨著需要而擴展,并且永遠不會發生垃圾收集,它只包含那些度過了 JVM 實例整個生命期的對象。
避免堆失效
如果使用大小可變的堆(比如,-Xms 和 -Xmx 不同),應用程序可能遇到這樣的情況,不斷出現分配失敗而堆沒有擴展。這就是堆失效,是由于堆的大小剛剛能夠避免擴展但又不足以解決以后的分配失敗而造成的。通常,垃圾收集周期釋放的空間不僅可以滿足當前的分配失敗,而且還有很多可供以后的分配請求使用的空間。但是,如果堆處于失效狀態,那么每個垃圾收集周期釋放的空間剛剛能夠滿足當前的分配失敗。結果,下一次分配請求時,又會進入垃圾收集周期,依此類推。大量生存時間很短的對象也可能造成這種現象。
避免這種循環的一種辦法是增加 -Xminf 和 -Xmaxf 的值。比方說,如果使用 -Xminf.5 ,堆將增長到至少有 50% 的自由空間。同樣,增加 -Xmaxf 也是很合理。如果 -Xminf.5 等于 5,-Xmaxf 為默認值 0.6,因為 JVM 要把自由空間比例保持在 50% 和 60% 之間,所以就會出現太多的擴展和收縮。兩者相差 0.3 是一個不錯的選擇,這樣 -Xmaxf.8 可以很好地匹配 -Xminf.5 。
如果記錄表明,需要多次擴展才能達到穩定的堆大小,但可以更改 -Xmine ,根據應用程序的行為來設置擴展大小的最小值。目標是獲得足夠的可用空間,不僅能滿足當前的請求,而且能滿足以后的很多請求,從而避免過多的垃圾收集周期。-Xmine 、-Xmaxf 和 -Xminf 為控制應用程序的內存使用特性提供了很大的靈活性。
標記棧溢出
使用 verbosegc 最重要的一項檢查是沒有出現“mark stack overflow”消息。下面的記錄顯示了這種消息及其影響。
<AF[50]: Allocation Failure. need 272 bytes, 18097 ms since last AF>
<AF[50]: managing allocation failure, action=1 (0/190698952)
(9584432/10036784)>
<GC(111): mark stack overflow>
<GC(111): freed 77795928 bytes in 1041 ms, 43% free (87380360/200735736)>
<GC(111): mark: 949 ms, sweep: 92 ms, compact: 0 ms>
<GC(111): refs: soft 0 (age >= 32), weak 0, final 0, phantom 0>
<AF[50]: completed in 1042 ms>
|
在垃圾收集的標記階段,如果引用的個數造成 JVM 內部的“標記棧”溢出,就會引發這種消息。在標記階段,垃圾收集處理代碼使用這個棧壓入所有已知的引用,以便遞歸掃描每個活動引用。溢出的原因是堆中的活動對象過多(或者更準確地說,對象嵌套過深),這通常表明應用程序代碼存在缺陷。除非能夠通過外部設置控制應用程序中的活動對象個數(比如某種對象池),那么需要在應用程序源代碼中解決這個問題。建議使用分析工具確定活動的引用。
如果不能避免大量的活動引用,并發標記可能是一種可行的選擇。
擺脫 finalizer
下面的記錄顯示了一種有趣的情況:解決分配失敗花費了 2.78 秒鐘,其中還不包括壓縮所用的時間。
<AF[1]: Allocation Failure. need 56 bytes, 0 ms since last AF>
<AF[1]: managing allocation failure, action=1 (0/521140736)
(3145728/3145728)>
<GC(1): GC cycle started Thu Aug 29 19:25:45 2002
<GC(1): freed 321890808 bytes, 61% free (325036536/524286464), in 2776 ms>
<GC(1): mark: 2672 ms, sweep: 104 ms, compact: 0 ms>
<GC(1): refs: soft 0 (age >= 32), weak 11, final 549708, phantom 0>
<AF[1]: completed in 2780 ms>
|
罪魁禍首是必須被結束掉的對象數量。無論如何,使用 finalizer 不是一個好主意,雖然在特定的情況下這是不可避免的,但是應該僅僅將它作為完成其他方法不能實現的操作的不得已方法。比方說,無論如何都要避免在 finalizer 內部執行分配。
避免非常大的分配
有時候問題不是由當時的堆狀態造成的,而是因為分配失敗造成的。比如:
<AF[212]: Allocation Failure. need 912920 bytes, 34284 ms since last AF>
<AF[212]: managing allocation failure, action=2 (117758504/261028856)>
<GC(273): freed 65646648 bytes in 2100 ms, 70% free (183405152/261028856)>
<GC(273): mark: 425 ms, sweep: 89 ms, compact: 1586 ms>
<GC(273): refs: soft 0 (age >= 32), weak 0, final 0, phantom 0>
<GC(273): moved 755766 objects, 43253888 bytes, reason=0, used x4C0 more
bytes>
<AF[212]: completed in 2101 ms>
|
這些記錄來自一個非常老的 JVM(準確地說是 ca130-20010615 ),因此壓縮的原因(紅色顯示)顯示為 0。但是壓縮 256 MB 的堆花費了 1.5 秒!為何這么差?再來看一看最初的請求,最初請求的是 912920 字節 —— 將近 1 MB。
分配的內存塊都必須是連續的,而隨著堆越來越滿,找到較大的連續塊越來越困難。這不僅僅是 Java 的問題,使用 C 中的 malloc 也會遇到這個問題。JVM 在壓縮階段通過重新分配引用來減少碎片,但其代價是要凍結應用程序較長的時間。上面的記錄表明已經完成了壓縮階段,分配一大塊空間的總時間超過了 2秒。
下面的記錄說明了最糟糕的一種情況。
<AF[370]: Allocation Failure. need 2241056 bytes, 613 ms since last AF>
<AF[370]: managing allocation failure, action=2 (135487112/1291844600)>
<GC: Wed Oct 16 10:16:46 2002
<GC(455): freed 41815176 bytes in 28663 ms, 13% free (177302288/1291844600)>
<GC(455): mark: 3233 ms, sweep: 328 ms, compact: 25102 ms>
<GC(455): refs: soft 0 (age >= 32), weak 0, final 17, phantom 0>
<GC(455): moved 15822115 objects, 615093008 bytes, reason=1, used x698
more bytes>
<AF[370]: managing allocation failure, action=3 (177302288/1291844600)>
<AF[370]: managing allocation failure, action=4 (177302288/1291844600)>
<AF[370]: clearing all remaining soft refs>
<GC(456): freed 176216 bytes in 3532 ms, 13% free (177478504/1291844600)>
<GC(456): mark: 3215 ms, sweep: 317 ms, compact: 0 ms>
<GC(456): refs: soft 16 (age >= 32), weak 0, final 0, phantom 0>
<GC(457): freed 9592 bytes in 23781 ms, 13% free (177488096/1291844600)>
<GC(457): mark: 3231 ms, sweep: 315 ms, compact: 20235 ms>
<GC(457): refs: soft 0 (age >= 32), weak 0, final 0, phantom 0>
<GC(457): moved 2794668 objects, 110333360 bytes, reason=1, used x30 more
bytes>
<AF[370]: managing allocation failure, action=5 (177488096/1291844600)>
<AF[370]: totally out of heap space>
<AF[370]: completed in 268307 ms>
|
請求的是一個 2 MB 的對象(2241056 bytes),雖然在 1.2 GB 的堆(1291844600)中有 135 MB (135487112) 自由空間,但卻不能分配一個 2 MB 的塊。雖然進行了一切可能的搜索,花費了 268 秒,但仍然沒有找到足夠大的塊。而且還出現了糟糕的“堆空間嚴重不足”消息,指出 JVM 的內存不足。
最好的辦法是:如果可能的話,把分配請求分解成較小的塊。這樣,較大的堆空間可能會起作用,但多數情況下,這樣做只是推遲了問題出現的時間。
碎片及其成因
我們再看一看上例中的其中一行:
<GC(455): freed 41815176 bytes in 28663 ms, 13% free
(177302288/1291844600)>
|
雖然有 177 MB 的自由空間,卻不能分配 2 MB 的塊。原因在于:雖然垃圾收集周期可以壓縮堆中的孔洞,但是堆中有些內容不能在壓縮過程中重新分配。比如,應用程序可能使用 JNI 分配和引用對象或數組。這些分配在內存中是固定的,既不能被重新分配,也不能被回收,除非使用適當的 JNI 調用來釋放它們。IBM Java 服務團隊可以幫助確定這類引用,在這種情況下,分析工具也大有用武之地。
類似地,因為類塊是在堆的外部引用的,因此也是固定的。即使沒有固定的對象,大的分配一般也會導致出現碎片。所幸的是,這類嚴重的碎片很少出現。
需要并發標記嗎?
如果由于垃圾收集造成 Java 應用程序不時地停頓,并發標記可以幫助減少停頓的時間,使應用程序運行更平穩。但有時候,并發標記可能會降低應用程序的吞吐能力。建議分別使用和禁止并發標記,使用相同的負荷來測量對應用程序性能的影響,并加以比較。
但是,觀察并發標記運行的 verbosegc 輸出可能提供大量關于加速的信息。不需要分析打印出來的記錄的每一部分,有意義的部分包括并發標記能夠成功掃描的概率(EXHAUSTED 和 ABORTED/HALTED),以及后臺線程能夠做多少工作。
下面的三個記錄屬于同一個 Java 應用程序,是在一次運行中的不同階段創建的,它們說明了并發運行的三種不同結果。
第一種可能的結果是并發標記得到 EXHAUSTED:
<CON[3]: Concurrent collection, (3457752/533723648) (3145728/3145728),
23273 ms since last CON>
<GC(246): Concurrent EXHAUSTED by Background helper . Target=82856559
Traced=57287216 (3324474+53962742) Free=3457752>
<GC(246): Cards cleaning Done. cleaned:13668 (33 skipped). Initial count
13701 (Factor 0.142)>
<GC(246): GC cycle started Tue Oct 1 00:05:56 2002
<GC(246): freed 436622248 bytes, 82% free (443225728/536869376), in 218 ms>
<GC(246): mark: 51 ms, sweep: 167 ms, compact: 0 ms>
<GC(246): In mark: Final dirty Cards scan: 43 ms 158 cards (total:127 ms)
<GC(246): refs: soft 0 (age >= 32), weak 0, final 5, phantom 0>
<CON[3]: completed in 230 ms>
|
這表明并發標記按照我們所預期的那樣工作。EXHAUSTED 意味著后臺線程能夠在出現分配失敗之前完成自己的工作。因為后臺線程掃描了 3324474 個字節(而應用程序線程掃描了 53962742 個字節),后臺線程能夠獲得足夠的 CPU 時間來減少總的標記時間。因此,STW 中的標記階段只用了 51 毫秒(ms),總的 STW 時間也不過 230 毫秒。這對于 512 MB 的堆來說,這已經很不錯了。
下面是 ABORTED 并發標記運行:
<AF[164]: Allocation Failure. need 962336 bytes, 75323 ms since last AF>
<AF[164]: managing allocation failure, action=1 (83408328/533723648)
(3145728/3145728)>
<GC(247): Concurrent ABORTED. Target=84703195 Traced=0 (0+0) Free=83408328>
<GC(247): GC cycle started Tue Oct 1 00:06:22 2002
<GC(247): freed 350077400 bytes, 81% free (436631456/536869376), in 896 ms>
<GC(247): mark: 695 ms, sweep: 201 ms, compact: 0 ms>
<GC(247): refs: soft 0 (age >= 32), weak 0, final 7, phantom 0>
<AF[164]: completed in 912 ms>
<CONCURRENT GC Free= 11530600 Expected free space= 11526488 Kickoff=11530370>
< Initial Trace rate is 8.00>
|
這是最糟糕的情況。并發標記被終止,主要是因為要分配大型對象和調用了 System.gc()。如果應用程序頻繁地這樣做,那么就不能從并發標記中獲得好處。
最好是 HALTED 并發標記:
<AF[168]: Allocation Failure. need 139280 bytes, 25520 ms since last AF>
<AF[168]: managing allocation failure, action=1 (11204296/533723648)
(3145728/3145728)>
<GC(251): Concurrent HALTED (state=64). Target=118320773 Traced=35469830
(14765196+20704634) Free=11204296>
<GC(251): No Dirty Cards cleaned (Factor 0.177)>
<GC(251): GC cycle started Tue Oct 1 00:08:06 2002
<GC(251): freed 385174400 bytes, 74% free (399524424/536869376), in 389 ms>
<GC(251): mark: 274 ms, sweep: 115 ms, compact: 0 ms>
<GC(251): In mark: Final dirty Cards scan: 46 ms 2619 cards (total:225 ms)
<GC(251): refs: soft 0 (age >= 32), weak 0, final 6, phantom 0>
<AF[168]: completed in 414 ms>
|
從并發標記的應用來看,HALTED 介于 EXHAUSTED 和 ABORTED 之間,它表明只完成了部分工作。上面的記錄說明,在進行下一次分配失敗之前,沒有完成掃描。在垃圾收集周期中,標記階段花費了 274 毫秒,總的時間上升到 414 毫秒。
在理想的情況下,多數垃圾收集周期都并發收集(由于并發標記完成其工作而觸發,標記為 EXHAUSTED),而不是出現分配失敗。如果應用程序調用 System.gc(),記錄中會出現很多 ABORTED 行。
在多數應用程序中,并發標記都可以改善性能,對于“標記棧溢出”也有所幫助。但是,如果標記棧溢出是由于缺陷造成的,惟一的解決辦法就是修正缺陷。
應該避免的開關
下列命令行開關應避免 使用。
命令行開關 |
說明 |
-Xnocompactgc |
該參數完全關閉壓縮。雖然在性能方面有短期的好處,最終應用程序堆將變得支離破碎,即使堆中有足夠的自由空間也會導致 OutOfMemory 錯誤 |
-Xcompactgc |
使用該參數將導致每個垃圾收集周期都執行壓縮,無論是否有必要。JVM 在壓縮時要做大量的決策,在普通模式下會推遲壓縮 |
-Xgcthreads |
該參數控制 JVM 在啟動過程中創建的垃圾收集幫助器線程個數。對于 N-處理器機器,默認的線程數為 N-1。這些線程提供并行標記和并行清理模式中的并行機制 |
結束語
本文簡單介紹了 IBM JVM 的垃圾收集和堆管理能力。以后的 verbosegc 日志很可能提供更多有用的信息。
總結一下文中提出的建議:
- 只要可能就升級到最新的 JVM 版本。您遇到的錯誤可能已經被發現并解決了。
- 調整
-Xms 、-Xmx 和 -Xminf ,直到 verbosegc 輸出給出分配失敗數量與每次垃圾收集造成的停頓數量之間的一個可接受平衡。使用固定大小的堆避免收縮或擴展。
- 如果可能的話,將較大的(>500 KB)塊分解成更小的塊。
- 不要忽略“標記棧溢出”消息。
- 避免使用 finalizer。
- 試一試并發標記。
- 問問是否有必要調用 System.gc(),如果沒有必要則刪除它。
如您所見,這個話題可不是三言兩語就能說明白的。但是,只需要打一個電話或者發一封電子郵件,就能與您的好伙伴 IBM Technical Support Team 取得聯系(參考資料中的鏈接是很好的起點)。對于您遇到的特殊情況,他們要比任何文章都更清楚。
參考資料
關于作者
|