Java性能優化的策略和常見方法(二)

1)JVM對堆空間的管理

JVM 在初始化的過程中分配堆。堆的大小取決於指定或者默認的最小和最大值以及堆的使用情況。如果用Heapbase表示堆底,heaptop表示堆能夠增長到 的最大絕對值,用heaplimit表示實際的堆頂;則兩者的差值(heaptop - heapbase)由命令行參數 -Xmx 決定。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),因爲固定大小的堆不能擴展或收縮。

2)基本收集算法

  • 複製:將堆 內分成兩個相同空間,從根(ThreadLocal的對象,靜態對象)開始訪問每一個關聯的活躍對象,將空間A的活躍對象全部複製到空間B,然後一次性回 收整個空間A。因爲只訪問活躍對象,將所有活動對象複製走之後就清空整個空間,不用去訪問死對象,所以遍歷空間的成本較小,但需要巨大的複製成本和較多的 內存。
  • 標記清除(mark-sweep):收集器先從根開始訪問所有活躍對象,標記爲活躍對象。然後再遍歷一次整個內存區域,把所有沒有標記活躍的對象進行回收處理。該算法遍歷整個空間的成本較大暫停時間隨空間大小線性增大,而且整理後堆裏的碎片很多。
  • 標記整理(mark-sweep-compact):綜合了上述兩者的做法和優點,先標記活躍對象,然後將其合併成較大的內存塊。

3)分代

分代是Java垃圾收集的一大亮點,根據對象的生命週期長短,把堆分爲年輕代和年老代,根據不同代的特點採用不同的收集算法。

年輕代(New Area
實 際上大部分對象都是朝生暮死,隨生隨滅的,因此所有收集器都爲年輕代選擇了複製算法。複製算法優點是隻訪問活躍對象,缺點是複製成本高。因爲年輕代只有少 量的對象能熬到垃圾收集,因此只需少量的複製成本。而且複製收集器只訪問活躍對象,對那些佔了最大比率的死對象視而不見,充分發揮了它遍歷空間成本低的優 點。年輕代隨堆內存增大而增大,JVM會根據情況動態管理其大小變化。-Xmns<value>, -Xmnx<value>, -Xmos<value>, -Xmox<value> 等JVM選項可以設置年輕代與年老代的初始尺寸和最大尺寸。

年輕代裏面又分爲2個區域,一個是Allocate區,所有新建對象都會存 在於該區,另一個是Survivor區;並實施複製算法。每次複製就是將Allocate中的活對象複製到Survivor或者年老代中(如果符合一定的 年老化條件),然後將Allocate區與Survivor區的角色互換。

年老代(Tenured Area)

 

分代

 

年輕代的對象如果能夠經歷過數次收集,就會進入年老區。年老區使用標記整理算法。因爲年老區的對象通常有較長的生命週期,採用複製算法就要反覆地複製對象,很不合算,所以採用標記清理算法。

年老期限(Tenure age)
年 老期限是用於衡量一個年輕代的對象在什麼情況下被升級爲年老代的對象。這個參數會被JVM動態調整,並達到一個最大值14。每次垃圾收集之後存活下來的對 象的年老期限會遞增一。一個年老期限爲x的對象意味着,當該對象經歷了Allocate區和Survivor區的x次反轉後仍然存活,則該對象會被升級爲 年老代對象。該閾值的調整是基於年輕代空間所佔堆空間的比例。

傾斜比率(Tilt ratio)
Allocate 區在年輕代區域中佔用的空間是使用一種稱爲Tilting的技術進行最大化的。Tilting控制Allocate區和Survivor區的相對大小。基 於每次反轉之後存活下來的對象所佔空間的總數,該傾斜比率(Tilt ratio)會被調整以使Survivor區變得更小。比如,如果初始年輕代的大小爲500MB,那麼Allocate區和Survivor區將各佔一 半,即250MB。隨着應用程序的運行,一次垃圾收集事件被觸發,而且只有50MB的對象存活下來。在這種情況下,Survivor區的空間將被減少,從 而爲Allocate區提供更多的空間。較大的Allocate區意味着將經歷更長的時間纔會發生下一次垃圾收集。如下圖所示,Survivor區的空間 會被逐步調整到最合適的比例。

垃圾收集前後的Allocate區和Survivor區的分佈舉例

 

分佈舉例

 

4)verbosegc日誌輸出

verbosegc日誌由 JVM 在指定 -verbosegc 命令行參數時生成,是一種非常可靠的獨立於平臺的調試工具。啓用 verbosegc 可能對應用程序的性能有一定影響。如果這種影響是無法接受的,則應該使用測試系統來收集 verbosegc 日誌。這是監控整個 JVM 是否運轉良好的一種好辦法,在出現 OutOfMemory 錯誤的情況下,這種方法尤其重要。

5)正確設置堆的大小

計 算正確的堆大小參數很容易,但它可能對應用程序啓動時間和運行時性能有很大的影響。初始大小和最大值分別由參數 -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 的堆。

垃圾收集週期所花費的時間直接與堆的大小成正比。一條好的原則是根據需要設置堆的大小,而不是將它配置得太大或太小。

常 見的一種性能優化技術是將初始堆大小(-Xms)設成與最大堆大小(-Xmx)相同。因爲不會出現堆擴展和堆收縮,所以在某些情況下,這樣做可以顯著地改 善性能。通常,只有需要處理大量分配請求的應用程序時,纔在初始和最大堆大小之間設置較大的差值。但是要記住,如果指定 -Xms100m -Xmx100m,那麼 JVM 將在整個生命期中消耗 100 MB 的內存,即使利用率不超過 10%。

6)避免堆失效

如 果使用大小可變的堆(比如,-Xms 和 -Xmx 不同),應用程序可能遇到這樣的情況,不斷出現分配失敗而堆沒有擴展。這就是堆失效,是由於堆的大小剛剛能夠避免擴展但又不足以解決以後的分配失敗而造成 的。通常,垃圾收集週期釋放的空間不僅可以滿足當前的分配失敗,而且還有很多可供以後的分配請求使用的空間。但是,如果堆處於失效狀態,那麼每個垃圾收集 週期釋放的空間剛剛能夠滿足當前的分配失敗。結果,下一次分配請求時,又會進入垃圾收集週期,依此類推。大量生存時間很短的對象也可能造成這種現象。避免 這種循環的一種辦法是增加 -Xminf 和 -Xmaxf 的值。比方說,如果使用 -Xminf.5,堆將增長到至少有 50% 的自由空間。同樣,增加 -Xmaxf 也是很合理。如果 -Xminf等於 5,-Xmaxf 爲默認值 0.6,因爲 JVM 要把自由空間比例保持在 50% 和 60% 之間,所以就會出現太多的擴展和收縮。兩者相差 0.3 是一個不錯的選擇,這樣 -Xmaxf.8 可以很好地匹配 -Xminf.5。

如果記錄表明,需要多次擴展才能達到穩定的堆大小,但可以更改 -Xmine,根據應用程序的行爲來設置擴展大小的最小值。目標是獲得足夠的可用空間,不僅能滿足當前的請求,而且能滿足以後的很多請求,從而避免過多的 垃圾收集週期。-Xmine、-Xmaxf 和 -Xminf 爲控制應用程序的內存使用特性提供了很大的靈活性。

7)應該避免的開關

下列命令行開關應避免使用:

-Xnocompactgc 該參數完全關閉壓縮。雖然在性能方面有短期的好處,最終應用程序堆將變得支離破碎,即使堆中有足夠的自由空間也會導致 OutOfMemory 錯誤
-Xcompactgc 使用該參數將導致每個垃圾收集週期都執行壓縮,無論是否有必要。JVM 在壓縮時要做大量的決策,在普通模式下會推遲壓縮
-Xgcthreads 該參數控制 JVM 在啓動過程中創建的垃圾收集幫助器線程個數。對於 N-處理器機器,默認的線程數爲 N-1。這些線程提供並行標記和並行清理模式中的並行機制
參考文獻:

<<IBM JDK 5.0 Diagnostics Guide>>
http://www-128.ibm.com/developerworks/cn/java/

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