Java SE 6 Hotspot 虛擬機器垃圾回收調優

發表於2012-08-03

來源:王旭

1. 概述

Java 平臺標準版(Java SE™)被廣泛應用於各種應用,從桌面上的小小的 applet 到大型伺服器上的 Web Service 無處不在。為了支援各種不同的部署場景,Java HotSpot™ 虛擬機器提供了多種垃圾回收器,每種都為滿足不同的需求而設定。這是也為了滿足大大小小不同應用需求的一部分。不過,那些需要高效能應用的使用者、開發者和管理員們也被選擇適合他們應用的恰當的垃圾回收器的繁瑣困擾著。取消這些額外操作的重要一步是在 J2SE™ 5.0 中作出的:垃圾回收器會根據應用執行的計算機型別而作出選擇。

這個垃圾回收器的“更好的選擇”總的說是一種進步,不過,這並不意味著對所有的應用這都是最好的選擇。對於有極端的效能或其他需求的使用者,仍需要顯式地指定垃圾回收器,並調優某些引數,以達到滿意的效能。本文就為這些需求提供了一些相關資訊。首先,本文會基於序列的 stop-the-world 垃圾回收器來介紹垃圾回收器的一般性特徵和基本調優開關。接下來會介紹其他垃圾回收器的特點和如何選擇一個垃圾回收器。

何時選擇垃圾回收器?對於一些應用,這個答案可能是“永遠不”。也就是說,在有低頻率、短時的垃圾收集器造成的停頓的情況下,大部分程式都執行良好。不過,這並不適用於很多程式,特別是那些處理大量資料(若干GB)、很多執行緒和需要處理很多事務的情況。

Amdahl 觀察到,大部分工作負載並不能被很好的並行化;有部分情況下總是會被順序執行,無法從並行化中獲益。這對 Java™ 平臺也是如此。特別的,在 J2SE 1.4 以前,Sun Java 平臺的虛擬機器並不支援並行垃圾回收,這樣,在多處理器系統中,垃圾回收會對並行應用產生嚴重影響。

下圖顯示了一個除了垃圾回收以外均為完美可伸縮的理想系統的效能曲線。紅色曲線是一個在但處理器系統中會花費 1% 的時間在垃圾回收上的程式。它在 32 處理器的系統中,將損失 20% 的吞吐量。而一個花費 10% 時間在垃圾回收上的應用(不考慮單處理器系統中額外的垃圾回收時間)在系統擴張到 32 處理器系統中時,會損失超過 75% 的吞吐量 。

Java SE 6 Hotspot 虛擬機器垃圾回收調優

這意味著在小型開發系統中微不足道的速度問題當擴張到大規模系統中就可能成為嚴重的效能瓶頸。從另一個角度看,減少這樣的效能瓶頸的小改動就可以獲得很大的效能收益。對足夠大規模的系統,選擇合適的垃圾收集器並進行必要調優是絕對值得的。

對於大多數“小”應用(在現代處理器上大約需要100MB堆記憶體的應用)來說通常是足夠的。其他垃圾收集器會帶來額外的負載或複雜性,這回讓系統的某些行為付出一定的代價。如果一個應用不需要一個垃圾收集器的某個功能。那麼就使用序列的垃圾收集器好了。一個不應該使用序列垃圾收集器場景是一個超多執行緒的大程式執行在一個大型的、有大量記憶體和兩個或多個處理器的系統中。當應用執行在這些伺服器級的計算機上的時候,並行垃圾收集器會被預設選擇(參見下面的功效學 )。

本文以 Solaris™ 作業系統(SPARC(R) 平臺版本)中的 Java SE 6 作為參考。不過,文中所述的概念和建議適用於所有支援的平臺,包括 Linux, Microsoft Windows 和 Solaris 作業系統(x86 平臺版本)。此外,文中的命令列引數也對所有平臺有效,雖然它們的預設值在各個平臺可能有所不同。

2. 功效學(Ergonomics)

“功效學”是一個 J2SE 5.0 引入的概念。引入功效學概念是為了通過不設定或設定很少的幾個命令列引數的情況下提供更好的效能,這些引數包括:

● 垃圾收集器,

● 堆尺寸,

● 和執行時編譯器

這裡的引數選擇假定應用所執行的主機型別和應用的型別一致(也就是說,大型應用執行在大型的機器上)。這些選項簡化了垃圾回收的調優。選擇並行垃圾回收器,使用者可以指定應用的最大中斷時間和希望的吞吐量。這和指定堆大小來調優效能是相對應的。最常用的功效學相關的內容在可以參考 “Ergonomics in the 5.0 Java Virtual Machine” 這篇文章。建議在嘗試本文提到的細節配置之前嘗試該文章中介紹的功效學手段。

本文中的功效學特性被作為並行垃圾回收器的自適應尺寸策略的一部分。這包括指定垃圾回收效能的目標和效能調優的一些附加選項。

3.代

J2SE 平臺的優勢之一是它將記憶體分配、垃圾回收這些繁複的細節遮蔽了起來。然而,一旦垃圾回收成為主要的瓶頸,那麼理解一下這些隱藏在背後的細節就變得有必要了。垃圾回收器對應用程式對物件的使用方式進行判斷,這個判斷會反映在可調優引數中,他們可以被調整,以提高效能而不犧牲掉抽象性。

當一個物件不再可能被從其他任何地方訪問到的時候就會被認為是垃圾了。最直接的垃圾回收演算法就是簡單地迭代所有可找到的物件。任何沒有被跌帶到的物件都可以被認為是垃圾了。這個方法的用時和活著的物件數量成正比,這對於那些維護著大量活資料的程式來說是不可接受的。

從 J2SE 1.2 開始,虛擬機器就引入了各種不同的垃圾回收演算法,這些演算法都使用分代垃圾收集。儘管原生的垃圾回收會檢查堆中的所有活著的物件,分代垃圾收集採用了很多觀測到的大部分應用程式的經驗特徵,用來最小化發現廢棄的物件的工作量。最重要的經驗特徵是 weak generational 假設,該假設認為大部分物件都只存活一少段時間。

下圖中的藍色區域是物件生存期的典型分佈。橫軸是物件被分配後的生存期。縱軸方向計算的位元組數是相應生存期的物件的總位元組數。左側的尖峰表明,物件在分配之後不久就被廢棄了。比如,迭代器物件常常只會在一個迴圈中被用到。

Java SE 6 Hotspot 虛擬機器垃圾回收調優

當然,有些物件確實活得要長一些,於是,分佈曲線延伸到了右邊。比如,典型情況下,有些物件在初始化的時候被建立,並一直存活到程式結束。在這兩種極限情況之間,那些物件活的時間也是中等的,在圖中表現初來的就是從開始的峰值洩漏初來的藍色區域。有些應用可能會有看起來十分不同的分佈曲線,不過絕大多數的程式都是這個常見的形狀。大部分物件都會“英年早逝”這個事實讓高效的垃圾收集變得具有可能性了。

為了為這樣的應用環境優化,記憶體被按照“代” (generation)進行管理,或者說,記憶體池中存放不同年齡的物件。當一個年齡斷被填滿後,就對該代的垃圾進行回收。在記憶體池中的大部分物件都是年輕的物件(年輕的代),而大部分物件也會在年輕的時候就成為垃圾。當年輕代被填滿的時候,會導致一次“小回收”(譯註:原文minor,似乎“未成年”更貼切一些,不過我們們讀起來會很彆扭),這裡只有年輕代的物件惠北迴收,而其他年齡斷的垃圾則不與理會。該回收演算法的成本是,一階情況下,正比於被回收的活的物件的數量;年輕代因為滿是死物件,所以回收非常迅速。而在“小回收”中存活下來的物件於是乎就會被轉移到所謂的年老代(tenured generation)。最終,當年老代被填滿而需要回收的時候,就會導致一次主回收,這時整個堆都會被回收。主回收通常會執行鎝比小回收慢很多,因為大量的物件都會被處理。

如上文記述,對不同的應用,“工效學”會動態選擇垃圾收集器來提供較好的效能。序列垃圾收集器用於哪些資料量比較小的程式,而且它的預設引數也讓大多數小程式能夠高效工作。而大吞吐量垃圾收集器用於那些有中到大資料量的資料集。工效學選擇的堆尺寸引數和自適應尺寸策略用於為伺服器提供更好的效能。這些選擇的大多數而不是所有的情況下工作得很不錯。這就引出了本文的核心宗旨:

如果垃圾收集器成為了瓶頸,你可能不得不調整整個堆的大小乃至每個代的尺寸。檢查垃圾收集器的詳細輸出,然後檢查垃圾收集器對你關注的各個效能指標的影響。

(並行垃圾收集器之外的)預設的代排布大概就是這樣的。

Java SE 6 Hotspot 虛擬機器垃圾回收調優

初始化的時候,最大的地址空間虛擬地保留住而沒有分配出去,直到真的需要的時候為止。整個保留的物件地址空間被分給了年輕的和年老的代。

年輕代包括“伊甸園”和兩個倖存者空間。大部分物件最初在伊甸園裡被分配出來。一個倖存者空間在任意時刻都是空的,作為伊甸園中的活物件的目的地,另一個是用於下一次收集。物件在倖存者空間之間停留到足夠老之後,就會被複制到年老代去了。

另一個和年老代有密切關係的代是永久的(permanent)代,這裡儲存著虛擬機器需要的用來描述那些 Java 語言層面沒有等價物的物件。比如,那些描述類和方法的的物件就存放在永久代。

3.1 效能考慮

對於垃圾回收的效能,主要有兩種量度方法:

1.吞吐量。吞吐量是在一段足夠長的時間中,沒有花費在垃圾回收上的時間佔總時間的百分比。吞吐量包含了花在空間費配上的時間(不過空間分配速度的調優一般是沒有必要的)。

2.延時。延時是由於等待垃圾回收而導致的程式沒有響應的時間。

不同的使用者對垃圾收集有不同的需求。比如,對於一個web server而言,吞吐量是合理的量度,因為垃圾收集帶來的短時時延是可以容忍的,或者說是很容易就被網路時延所掩蓋了。不過,對於互動的圖形介面程式而言,極短的停頓都會影響使用者的使用體驗。

有些使用者對其他的因素很敏感。Footprint是一個程式的工作集,由頁和cache line來量度。對於記憶體相對於程式數量很有限的系統而言。Footprint會影響到程式的可伸縮性。Promptness是物件死掉和該塊記憶體重新可用之間的時間間隔的量度,這是分散式系統的一個重要考慮因素,包括遠端方法呼叫(RMI)。

總的說,一個特定的代的尺寸選擇是上述這些因素之間的權衡的結果。比如,一個非常大的年輕代的大小可以最大化吞吐律,但會以Footprint、Promptness和延時作為代價。而年輕代延時可以通過縮小該代的大小來達到最小化,但同樣會損失吞吐量。近似地,調整一個代的尺寸不會影響到其他代的垃圾收集頻率和時延。

沒有一個簡單的方法來設定代的尺寸。最好的選擇由程式使用記憶體的方式和使用者的需求來決定。這樣,虛擬機器對垃圾收集器的選擇並不總是最優的,而且可以通過後面介紹的命令列引數來調整。

3.2 測量

使用應用特定的量度,吞吐量和footprint很容易被測量。例如,web伺服器的吞吐量可以使用一個客戶端負載生成器來測量,而該伺服器的 footprint 則可以在 Solaris 作業系統中使用 pmap 命令來測量。另一方面,垃圾收集導致的時延可以方便地通過監測虛擬機器自己的診斷輸出來估算出來。

命令列引數 -verbos:gc 可以送出每一次垃圾收集時的堆和垃圾收集資訊。比如,這是一個大型伺服器應用的輸出:

這裡是兩次小回收和之後的一次主回收。箭頭前後的數字(比如第一行的325407K->83000K)分別指垃圾回收前後的所有活著的物件佔用的空間。在小回收之後,這個尺寸之中仍然包含一些沒有被回收的垃圾(死掉的物件)。這些物件要麼存在在年老代中,要麼被年老或永久代中的物件所引用。

後面的括號中的數字(比如第一行中的 (776768K))是全部提交的堆大小,也就是虛擬己不向作業系統申請記憶體的情況下,全部 java 物件可用的儲存空間。注意,這個數字不包括倖存者空間中的一個,因為倖存者空間在一個給定時間只有一個可用,同時也不包括永久代的空間,這裡面是虛擬機器使用的後設資料。

最後一個數字(比如 0.2300771 secs)是垃圾收集所用的時間;這個例子裡大約是四分之一秒。

第三行中主垃圾回收的格式也是類似的。

-verbos:gc 輸出的格式可能在將來的版本里有所改變。

通過-XX:+PrintGCDetails引數可以檢視更多垃圾回收相關的資訊。下面是序列垃圾收集器使用該引數列印出來的資訊。

這個資訊顯示,這次小回收收回了 98% 的 DefNew 年輕代的資料,64575K->959K(64576K) 並在其上消耗了 0.0457646 secs(大約45毫秒)。

整個堆的佔用率下降了大約51% 196016K->133633K(261184K),而且通過最終的時間 0.0459067 secs 顯示在垃圾收集中有輕微的開銷(在年輕代之外的時間)。

選項-XX:+PrintGCTimeStamps會提供每次回收開始時間的時間戳。這對於檢視垃圾回收頻率非常有用。

如上,垃圾回收在程式執行後111秒開始。小回收同時啟動。資訊中還顯示了主回收中的年老代的垃圾回收資訊。年老代的空間使用率下降了大約 10% 18154K->2311K(24576K) ,用時 0.1290354(大約130毫秒)。

和 -verbose:gc 一樣,-XX:+PrintGCDetails 的輸出格式在將來的版本里也可能會有所變動。

4. 代的尺寸

很多引數會應想到代的尺寸。下圖是堆中的提交空間和虛擬空間的差別。虛擬機器初始化的時候,整個堆空間都是保留的。保留空間可以通過引數 -Xmx 指定。如果-Xms引數小於-Xmx引數,那麼不是所有的保留空間都會立刻提交到虛擬機器之中。未提交的空間在途中標記為 virtual。堆的不同部分(永久時間段、年老時間段和年輕時間段)可以按需生長到虛擬空間的限制為止。

一些引數可以調整堆的不同部分的比例,比如引數NewRatio指定年老代對年輕代的比例。這些引數將在下面討論。

Java SE 6 Hotspot 虛擬機器垃圾回收調優

4.1 全部堆

注意,下面的關於堆的生長、收縮和預設堆大小都不適用於並行垃圾收集器,並行垃圾收集器請參考相關章節。不過,用於控制整個堆大小和代尺寸的引數對並行垃圾收集器都是適用的。

因為垃圾收集是發生在代被填滿的時候,所以,吞吐量反比於可用此記憶體數量。總可用記憶體數是影響垃圾收集效能的最重要因素。

預設情況下,虛擬己在每次垃圾收集後增加或減少堆尺寸,來儘量保持可用空間對活的物件之間的比例在一個區間之內。這個目標區間通過引數-XX:MinHeapFreeRatio=<minimum>和-XX:MaxHeapFreeRatio=<maximum>來設定,而總的堆大小的界限由-Xms<min>和-Xmx<max>來確定。這些引數在 32 位 Solaris 系統(SPARC 平臺版本)中的預設值如下表所示:

64位系統中的堆尺寸的引數會大 30% 左右,這個增長用來補償64位系統中更大的物件所帶來的開銷。

通過設定這些引數,當一個代的可用空間低於 40%,虛擬機器就會把可用記憶體擴充套件到 40%,直到代的最大尺寸。同樣的,如果可用空間超過 70%,代就會被縮小,使得只有 70% 可用空間,直到達到代最小的空間為止。

大型伺服器程式在使用這些預設設定時,經常遇到兩種問題。其一是慢啟動問題,初始的堆尺寸過小,經常需要經歷多次主回收才能達到穩定值。另一個更現實的問題是,對於大多數伺服器應用來說,這個預設的最大堆大小太小了。對於伺服器程式而言,設定的一般原則是:

● 除非遇到了時延問題,給虛擬機器儘量多的記憶體。預設尺寸(64MB)通常都太小了。

● 把-Xms 和 -Xmx 設定成相同的值,把最重要的尺寸決定從虛擬機器收回來,從而增強可預見性。

● 一般地,隨著處理器數量的增加而增加記憶體,因為記憶體分配可以被並行化。

作為參考,有一個單獨的頁面會介紹各個命令列引數 。

4.2 年輕代

影響位居次席的是用於年輕代的堆比例。年輕代越大,小回收的次數也就越少。不過,在一定的堆大小的情況下,年輕代越大,年老代也就越小,這就增加了主回收的頻率。最佳選擇依賴於應用中分配的物件的生存期分佈。

預設的,年輕代的尺寸由 NewRatio 控制。比如,設定-XX:NewRatio=3意味著年輕代和年老代的比例是1:3。換句話說, eden 和倖存者空間的總和是整個堆大小的四分之一。

引數 NewSize 和 MaxNewSize 約束了年輕代的上下界限。可以把這兩個引數設成相同的值來固定年輕代的大小,設定 -Xms 和 -Xmx 一樣來設定堆大小為固定值。這樣可以比使用NewRatio更細粒度地調整年輕代的大小。

4.2.1 倖存者空間

如果需要,SurvivorRatio 可以用來調整倖存者空間的大小,不過這對於效能一般影響不大。比如,-XX:SurvivorRatio=6 會設定倖存者空間和eden的比例是 1:6。換句話說,每個倖存者空間將是 eden 的六分之一,是整個年輕代空間的八分之一(不是七分之一,因為一共有兩個倖存者空間)。

如果倖存者空間過小的話,拷貝收集到的倖存者將會直接溢位到年老代的空間中去。如果倖存者空間太大的話,他們也就是空著浪費掉。每次垃圾收集中,虛擬機器會選擇一個物件在成為年老的之前被複制的次數門限。這個門限的設定會保證倖存者空間是半滿的。命令列引數-XX:+PrintTenuringDistribution 可以顯示這個門限和年輕代中物件的年齡。這對於觀測應用中物件的生存期分佈也是有用的。

下面是 SPARC 上的 32 位 Solaris 的各個引數的預設值,在其他平臺上可能有所差異。

年輕代的最大尺寸通過最大堆尺寸和 NewRatio 計算而得。所謂的“無限制”的預設值是說這個計算的值不會受到 MaxNewSize 的約束,除非命令列中指定了這個值。

服務應用的設定準則是:

● 首先確定可以提供給虛擬己的最大堆尺寸。然後根據效能需求來確定年輕代的尺寸,來找到最佳設定。

● 注意:最大堆尺寸一定要小於系統中的記憶體數量,以防止過多的缺頁錯誤和換頁。

● 如果總的堆尺寸是確定的,增加年輕代的尺寸就會減少年老代的尺寸。一定要保證年老代的尺寸,使之可以容納所有在應用全程都要用到的活物件,並留有一定裕量(10-20%或更多)。

● 依照上述年老代的約束:

● 給年輕代分配足夠的記憶體。

● 如果有多個處理器,那麼分配更多的記憶體給年輕代,因為記憶體分配可以並行化。

5. 可用的垃圾收集器

到目前為止,我們討論的還都是序列垃圾收集器。不過 Java HotSpot 虛擬機器一共支援了三種不同的收集器,每種提供不同的效能特性。

1.序列垃圾收集器使用單執行緒進行所有垃圾收集工作,因為沒有執行緒間通訊的開銷,序列垃圾收集器相當高效。序列垃圾收集器最適合於單處理器系統,因為它不會從多處理器硬體中獲益,儘管在小資料量的應用中(不大於100MB的),它對於多處理器系統也是游泳的。序列垃圾收集器在一定的硬體和作業系統的配置時會預設使用,也可以顯式地用 -XX:+UseSerialGC 引數來指定。

2。並行垃圾收集器(或吞吐垃圾收集器)並行進行小垃圾收集,這會顯著減少垃圾收集的的開銷。它適用於中等或大尺寸資料的執行在多處理器或多執行緒硬體上的應用。並行垃圾收集器也會在一定的硬體和作業系統配置下被預設使用,同時,也可以使用 -XX:+UseParallelGC 引數來指定。

● 更新:“並行壓縮”是 J2SE 5.0 update 6 以上版本的新特性,並在 Java SE 6 之中得到加強,該特性允許主回收也並行收集。如果不使用並行壓縮,主回收仍然會單執行緒執行,這會嚴重限制系統的可伸縮性。並行壓縮可以使用命令列引數-XX:+UseParallelOldGC 來開啟。

3併發垃圾收集器併發地進行大部分垃圾收集工作(也就是在應用執行當中進行)來儘可能煎炒垃圾收集帶來的應用停頓。它是為哪些擁有中到大量資料的、對響應時間要求高於吞吐量要求的應用,因為最小化時延的技術會讓吞吐能力付出代價。併發垃圾收集器通過 -XX:+UseConcMarkSweepGC 引數來啟用。

5.1 選擇垃圾收集器

除非你的應用有非常嚴酷的時延要求,那麼就執行你的應用,並讓系統自己選擇垃圾收集器好了。如果有必要的話,就調整堆的大小來增進效能。如果效能仍然無法達到你的目標,那就按照如下設定來選擇一個垃圾收集器。

1. 如果應用的資料很少(大約不超過100MB),那麼

● 使用-XX:+UseSerialGC選擇序列垃圾收集器。

2.如果應用執行在單處理器系統中,並且沒有什麼時延要求,那麼

● 讓虛擬機器選擇垃圾收集器,或者

● 使用-XX:+UseSerialGC選擇序列垃圾收集器。

3.如果(a)程式峰值效能是第一位的,並且(b)沒有時延要求,或時延要求是一兩秒或更長,那麼

● 讓虛擬機器選擇垃圾收集器,或者

● 使用-XX:+UseParallelGC選擇並行垃圾收集器,乃至(可選)通過 -XX:+UseParallelOldGC啟用並行壓縮。

4.如果響應時間比總體吞吐量更為重要,並且垃圾收集時延需要控制在1秒以內,那麼

● select the concurrent collector with -XX:+UseConcMarkSweepGC. If only one or two processors are available, consider using incremental mode, described below.

● 通過 -XX:+UseConcMarkSweepGC 引數啟用併發垃圾收集器。進當你有一個或兩個處理器可用的時候,考慮使用下文將要介紹的“增量模式”。

這些指導意見僅僅是選擇垃圾收集器的起點,因為效能依賴於堆的尺寸、應用中活資料的數量,以及處理器的數量和速度。時延引數對這些因素尤為敏感,所以,所謂的1秒門限值只是個大致數值:在很多硬體和資料量的組合情況下,並行垃圾收集器可能會導致停頓時間超過1秒;同樣,在某些組合下,併發垃圾收集器也不能保證停頓小於1秒。

如果推薦的垃圾收集器沒有達到期望的效能,首先應該嘗試堆和代的尺寸,以期達到目標。如果仍然不成功的話,嘗試更換一個垃圾收集器:使用併發垃圾收集器來減少停頓時間,使用並行垃圾收集器來增加多處理器系統中的吞吐量。

6. 並行垃圾收集器

並行垃圾收集器(也被稱為吞吐量收集器)和序列收集器類似,也是一種分代垃圾收集器;其最大的不同在於它使用了多執行緒來加快垃圾收集的過程。並行垃圾收集器可以通過引數 -XX:+UseParallelGC 指定。預設的,只有小回收會並行執行,主回收仍然單執行緒執行。不過,通過引數-XX:+UseParallelOldGC啟動並行壓縮可以讓主回收和小回收都並行執行,從而進一步減少垃圾收集開銷。

在一個有N個處理器的計算機上,並行垃圾收集器使用N個垃圾收集器執行緒。不過,這個數量可以在命令列引數裡指定(參見下文)。在一臺單處理器的計算機上,由於執行緒開銷(比如同步),並行垃圾收集器的效能應該不如序列垃圾收集器。然而,當應用程式有中等或大尺寸的堆的時候,它在一個雙處理器的機器上就會略優於序列垃圾收集器,而如果有多於兩個處理器的話,它就能遠勝於序列垃圾收集器。

垃圾收集器執行緒數的多少可以用-XX:ParallelGCThreads=<N>引數來控制。如果要使用命令列引數顯式調整了堆的尺寸,使用並行垃圾收集器的情況下需要的堆的尺寸和使用序列垃圾收集器情況下的堆的尺寸是一階相等的。使用並行垃圾收集器僅僅是讓小回收造成的停頓更短一些。因為有多個垃圾收集器執行緒參與小回收的過程,有極少的可能性可能會在將年輕代移動到年老代的過程中造成一些碎片。每個垃圾收集執行緒都有一塊專屬的年老代的空間,用於年輕代向年老代的移動,將年老代的可用空間劃分為“移動緩衝”(promotion buffer)的過程可能會造成一定的碎片效應。減少垃圾收集器執行緒的數量可以減少碎片、增加年老代的空間。

6.1 代

正如上面提到的,並行垃圾收集器的代的排布方式和序列垃圾收集器略有不同。其分佈如下圖所示。

Java SE 6 Hotspot 虛擬機器垃圾回收調優

6.2 功效學

自 J2SE 5.0 以來,並行垃圾收集器成為了server級機器的預設垃圾收集器,詳細資料可以參考“Garbage Collector Ergonomics”。此外,並行垃圾收集器使用一種自動調整機制來指定期望的行為而不是指定代的大小和其他底層調整細節。這些行為包括:

● 最大垃圾收集停頓時間

● 吞吐量

● Footprint (也就是堆尺寸)

最大停頓時間的目標由引數-XX:MaxGCPauseMillis=<N>來指定。這個引數被解釋為指定停頓時間不得大於<N>毫秒;預設情況下沒有最大停頓時間目標。如果指定了一個停頓時間目標,堆尺寸和其他垃圾回收相關引數就會被相應調整,以便保持垃圾回收時間小於指定的值。注意,這些調整可能會導致總體吞吐量的降低,而且,在某些情況下,要求的停頓時間目標可能無法達到。

吞吐量目標測量垃圾回收時間和非垃圾回收時間(也就是應用時間)的比例。這個目標時間可以用命令列引數-XX:GCTimeRatio=<N>來指定,這樣,垃圾回收時間和應用時間的比例將是1 / (1 + <N>)。例如,-XX:GCTimeRatio=19設定1/20活5%的時間用於垃圾回收。預設值是99,目標是1%的時間用於垃圾回收。

最大堆footprint使用已經存在的 -Xmx<N> 引數。此外,如果沒有其他的優化目標的話,垃圾收集器有一個隱式的最小化堆尺寸的目標。

6.2.1 目標的優先順序

目標的優先順序順序如下:

1.最大停頓時間目標

2.吞吐量目標

3.最小堆尺寸目標

最大停頓時間目標會被首先滿足。僅當最大停頓目標被滿足的情況下,才會去滿足吞吐量目標。類似的,僅當前兩個目標都會滿足的情況下,才會考慮去滿足footprint目標。

6.2.2 時間段尺寸調整

每次垃圾收集結束的時候,垃圾收集器都會更新其儲存的平均停頓時間之類的統計參量。同時它會檢查各個目標是否被滿足了,是否有調整代尺寸的需要。這之中的意外情況就是顯式的垃圾收集(比如呼叫 System.gc())會在統計和調整判斷中被忽略掉。

增加和縮小一個代的大小是通過增加活縮小一個固定的百分比來達到的,這樣一個代要分步來達到需要的尺寸。增加活所見是以不同的比率來進行的。預設情況下,一次增加 20% 活減少 5%。年輕代和年老代增量的比例分別通過命令列引數 -XX:YoungGenerationSizeIncrement=<Y>和-XX:TenuredGenerationSizeIncrement=<T>來設定。而縮小比例的要通過-XX:AdaptiveSizeDecrementScaleFactor=<D>引數來設定。如果增量是X%,那麼每次減小量就是(X/D)%。

如果垃圾收集器決定在啟動的時候增加一個代的大小,會有一個額外的百分比的增量。這個附加的增量隨著收集的次數而減少,不會長期影響。這個額外增量意在提高啟動速度。縮小代的尺寸是沒有這個額外的增量。

如果最大停頓時間目標沒有達到,會有且僅有一個代的大小被縮小。如果兩個代都在目標之上,停頓時間較大的那個代會首先被縮小。

如果總體吞吐量目標沒有達到,那麼兩個代的大小都會增加。每個都按照各自對垃圾回收時間的貢獻比例分別增加。比如,如果年輕代的垃圾回收時間佔去了25%的總垃圾回收時間,並且年輕代的全部增量應該是20%,那麼這時它的增量就是5%。

6.2.3 預設堆尺寸

如果沒有在命令列中進行設定,初始和最大堆尺寸會通過計算機記憶體計算而得。如下表所示,對大小佔用的記憶體的比例是由引數 DefaultInitialRAMFraction和DefaultMaxRAMFraction來控制的。(表中的 memory 代表計算機的系統記憶體數量。)

注意,預設的最大堆尺寸不會超過1GB,不論系統中到底有多少記憶體。

6.3 過多的GC時間和OutOfMemory錯誤

當有過多的時間花費在垃圾收集上的時候,並行垃圾收集器會跑出 OutOfMemoryError 錯誤:如果超過 98% 的時間花費在垃圾收集上並且只有 2% 的堆被釋放的話,就會丟擲一個 OutOfMemory。這個功能是用來防止堆太小導致程式長時間無法正常工作而設計的。如果必要,這個功能可以使用命令列引數-XX:-UseGCOverheadLimit來關閉。

6.4 測量

並行垃圾收集器的垃圾收集器詳細輸出和序列垃圾收集器是一樣的。

7. 併發垃圾收集器

併發垃圾收集器適用於那些需要更短的垃圾收集停頓,並能為此付出程式執行期處理器資源的應用。典型情況下,那些擁有較多長期存在的物件(年老代比較大),並且執行在擁有兩個活更多處理器的應用可能會因此獲益。不過,在任何要求很低停頓時間的應用都應該考慮這個垃圾收集器;比如,擁有較小年老代的互動程式在但處理器上使用併發垃圾收集器就可以收到明顯的好處,特別是使用增量模式的時候。併發垃圾收集器可以通過命令列引數-XX:+UseConcMarkSweepGC來啟動。

和其他垃圾收集器類似,併發垃圾收集器也是分代的;所以也有小回收和主回收。併發垃圾收集器通過使用獨立的垃圾收集執行緒於應用本身的執行緒併發執行跟蹤所有可及的物件,以期降低主回收導致的停頓。在每個主回收周其中,併發垃圾收集器會在垃圾收集的開始讓所有應用執行緒暫停一下,並在回收中段再暫停一次。第二次暫停相對而言會更長一些,在此期間會有多個執行緒來進行收集工作。剩下的收集工作包括大部分的活物件跟蹤和清除不可及的物件的工作都由一個或多個和應用併發的垃圾收集器執行緒來進行。小回收會在進行的主回收周其中穿插進行,其模式和並行垃圾收集器十分類似(特別需要說明的就是,在小回收期間,應用執行緒是會有停頓的)。

併發垃圾收集器的基本演算法在技術報告 A Generational Mostly-concurrent Garbage Collector裡有介紹。主義,實際的實現細節在不同版本里手有細微的變化的,因為垃圾收集器也在一直進步。

7.1 併發性的開銷

併發垃圾收集器的短主回收停頓時間是以處理器資源作為代價的(這些資源如果不用在收集器上肯定就要用在應用上了)。最明顯的開銷就是併發地使用了一個或多個處理器資源。在N處理器系統中,垃圾收集的併發部分會使用K/N的可用處理器,其中 1<=K<=ceiling{N/4}。(注意,K值的上限將來可能會有變化。)併發垃圾收集器不僅在併發階段使用處理器,還引入了其他的開銷。所以,儘管併發垃圾收集器顯著減少了程式的停頓,但和其他垃圾收集器相比,應用的總體吞吐量會受到輕微的影響。

在擁有多個處理器的計算機上,在併發垃圾收集器執行的時候,應用程式仍然能使用到CPU,所以,併發垃圾收集器並沒有讓程式停頓。這通常意味著更短的停頓,談也意味著更少的應用可用的處理器資源,並且讓它執行得相對比較慢,特別是當應用可以完全的利用多個CPU核心的時候更是如此。隨著N的上升,垃圾收集器導致的損失會相對變小,而從併發垃圾收集的獲益則相對提高。下一節“併發模式失敗”會討論這種規模擴張的潛在侷限。

因為在併發階段至少有一個處理器用於了垃圾收集,所以在單處理器(單核)系統中,併發垃圾收集器一般不會帶來什麼好處。不過,併發垃圾收集有一個分離模式可以在單處理器或雙處理器系統中顯著減少停頓時間;後面的增量模式中將會進一步介紹其細節。

7.2 併發模式失敗

併發垃圾收集器使用一個或多個垃圾收集執行緒在應用執行緒執行的同時執行,從而在年老代和永久代變滿之前就完成垃圾收集。如前文所述,在一般的操作中,併發垃圾收集器的大部分跟蹤與清理工作是在程式執行的同時進行的,所以,程式執行緒只有極少的停頓。但是,如果併發垃圾收集器在年老代變滿的時候仍沒有完成垃圾清除工作,或是年老代中的可用空間無法滿足一次分配操作的需要的時候,應用就不得不被暫停下來以等待應用執行緒結束了。這種無法併發地完成垃圾收集的情況被稱為“併發模式失敗”,這就需要對併發垃圾收集器的引數進行調整了。

7.3 過多的GC時間和OutOfMemory錯誤

併發垃圾收集器會在垃圾收集消耗時間過多的時候丟擲 OutOfMemoryError 錯誤:如果多於 98% 的時間被花費在了垃圾手機上,並且僅有少於 2% 的堆被回收的話,就會丟擲 OutOfMemoryError。這個功能是用來防止堆太小導致程式長時間無法正常工作而設計的。如果必要,這個功能可以使用命令列引數-XX:-UseGCOverheadLimit來關閉。

這個策略和並行垃圾收集器是基本一致的,惟一的區別就是併發的垃圾收集時間並未計算在內。也就是說,只有哪些程式停頓下來進行垃圾收集的時間才被計算在內了。這些垃圾收集常常是由於併發模式失敗或是顯式垃圾收集請求(如呼叫 System.gc())導致的。

7.4 浮動垃圾

併發垃圾收集器與 HotSpot 中的其他垃圾收集器一樣,是一種識別至少所有在堆中可以被訪問到的物件的跟蹤收集器。按照Jones and Lins的說法,是一種增量更新(Incremental Update)垃圾收集器。因為應用現成和垃圾收集器執行緒在主回收過程中併發執行,那麼那些垃圾收集器跟蹤的物件就可能在垃圾收集完成之後變成垃圾這些無法訪問卻還沒有被回收的物件被稱為浮動垃圾(floating garbage)。浮動垃圾的數量取決於垃圾收集週期的長度和程式中引用更新的頻率,也被稱為轉化率(mutation)。而且,另一個原因是年輕代和年老代的收集是獨立的,彼此都是對方的根。一個粗略的配置規則是為年老代的浮動垃圾多預留出20%的空間來。一個垃圾回收週期中的堆中的浮動垃圾會在下一個垃圾回收週期中被回收。

7.5 時延(停頓)

併發垃圾收集器在一個併發回收週期中會兩次暫停應用。第一次會從根從根(比如從物件執行緒棧和暫存器、靜態物件等的引用)和堆的其他部分(如年輕代)開始標記所有直接可達的活的物件。第一次停頓被稱為“初始標記停頓”(initial mark pause)。第二次停頓發生在併發跟蹤階段末尾,用來發現由於在垃圾收集執行緒跟蹤完一個物件之後又被應用執行緒更新了其引用而沒有被併發跟蹤到的物件。這次停頓被稱為“重標記停頓”(remark pause)。

7.6 併發階段

可達物件的併發跟蹤圖發生在初始標記停頓和重標記停頓之間。在併發跟蹤階段中,一個或多個併發垃圾收集器執行緒會使用那些本來可能會被應用使用的處理器資源,所以儘管不會停頓,計算密集型應用可能會在此階段和其他併發階段受到相當的吞吐量損失。在重標記停頓之後,還有一個併發清理階段,會收集所有標記為不可達的物件。一旦手機週期結束了,併發收集器就會進入等待階段,這時就基本不會消耗任何計算資源了,直到下一個主回收週期開始為止。

7.7 開始併發收集週期

在序列收集器中,每當年老代滿了的時候都會引發一次主回收,所有應用現成都會在主回收期間暫停執行。併發垃圾收集器與之不同,它需要在足夠早的時間開始垃圾收集,以便能在年老代變滿之前完成垃圾收集;否則的話就會因為併發模式失敗而導致較長的時延。有很多種條件可以觸發併發垃圾收集器啟動。

基於最近的歷史記錄,併發垃圾收集器維護了一個年老代變滿的預期剩餘時間和一個垃圾收集週期的預期時間。基於這些動態估計,併發垃圾收集週期會以讓垃圾收集週期在年老代變滿之前完成為目標開始併發垃圾收集週期。因為併發模式失敗的代價非常慘重,這些估值都流出了安全裕量。

併發垃圾收集在年老代的已用百分比超出了一個初始佔有率值(initiating occupancy)的時候也會啟動。這個初始佔有率閾值的預設值大約是 92%,不過這個值可能在不同版本中略有不同。它也可以通過命令列引數-XX:CMSInitiatingOccupancyFraction=<N> 來手工設定,其中N是一個0-100的整數,代表年老代的佔用百分比。

7.8 排程中斷

年輕代和年老代的垃圾收集的停頓發生彼此間是獨立的。他們不會重合,但可能會連續發生,這樣也就讓一個垃圾收集的停頓連上下一個垃圾收集的停頓了,從外界來看就是一個長停頓了。為了避免這種情況,併發垃圾收集器會排程重標記停頓的時間,使之發生在前後兩個年輕代停頓之間。這個排程目前還不應用於初始標記停頓,因為它通常會比重標記停頓短很多。

7.9 增量模式

併發垃圾收集器可以在這樣一種模式下工作:併發階段以增量的方式進行。回憶一下,在併發階段,垃圾回收執行緒會使用一個或多個處理器。所謂增量模式是指減少長併發階段的影響,週期性中斷併發階段,將處理器資源還給應用程式。這種模式又稱為“i-cms”,將垃圾收集器的併發工作劃分到小塊時間,在年輕代垃圾收集之間進行。這個功能對於那些工作在沒那麼多處理器的機器上(1或2個處理器的)需要併發垃圾收集器的低時延應用非常有用。

併發垃圾收集週期通常包括如下幾步:

● 停止所有的應用執行緒,標記從根開始可達的物件集,然後繼續所有的應用執行緒

● 在應用執行緒執行的同時,使用一個或更多的處理器,併發跟蹤可達的物件圖

● 使用一個處理器,併發跟蹤物件圖中在上一步開始之後的各個改動的部分

● 停止所有的應用執行緒,重新跟蹤根和物件圖中自從上次檢查開始發生了變化的部分,然後繼續執行執行緒

● 使用一個處理器,併發地把不可達物件清理到用於分配空間的 free list 上面去。

● 使用一個處理器併發地調整堆的大小,準備下一個回收週期所需的資料結構

正常情況下,併發垃圾收集器在併發跟蹤階段使用一個或多個處理器,不會讓出它們。類似的,在清理階段也會始終獨佔地使用一個處理器。這對於對於一個程式的響應時間可能是個不小的影響,特別是系統中只有一兩個CPU的時候。增量模式通過將併發階段分解為一系列的突發行為來降低這一影響,這些突發行為會散佈在小回收之間。

i-cms 使用佔空比來控制併發收集器自發的放棄處理器之前的工作量。佔空比是年輕代收集之間的允許併發垃圾收集器執行時間的百分比。i-cms 可以根據應用的行為自動計算佔空比(這也是推薦的方法,稱為自動步長(auto pacing)),當然,也可以通過命令列指定一個固定的值。

7.9.1 命令列引數

下面是控制 i-cms的命令列引數(參考下文的初始設定建議):

引數

描述

預設值

J2SE 5.0 及以前

Java SE 6 及以後

-XX:+CMSIncrementalMode

啟動增量模式。注意,併發垃圾收集器必須也被選擇(-XX:+UseConcMarkSweepGC) ,否則此引數無效。

disabled

disabled

-XX:+CMSIncrementalPacing

開啟自動步長,這樣,增量模式佔空比將根據JVM統計到的資訊自動調整。

disabled

enabled

-XX:CMSIncrementalDutyCycle=<N>

兩次小回收之間的允許併發收集器執行的時間的百分比(0-100)。如果開啟自動步長,那麼這個值就是初始值。

50

10

-XX:CMSIncrementalDutyCycleMin=<N>

自動步長開啟後,佔空比值的下限 (0-100)。

10

0

-XX:CMSIncrementalSafetyFactor=<N>

計算佔空比值時使用的一個裕量(0-100)

10

10

-XX:CMSIncrementalOffset=<N>

在小回收之間,增量模式中佔空比開始的時間,或說是向右的平移量(0-100)

0

0

-XX:CMSExpAvgFactor=<N>

當進行併發回收統計,計算指數平均值時,當前取樣所用的權值(0-100)

25

25

7.9.2 建議引數

要在 Java SE 6 裡使用 i-cms,需要使用如下命令列引數

前兩個引數分別啟動併發垃圾收集器和 i-cms。後兩個引數不是必須的,它們只是要求垃圾收集器將診斷資訊列印到標準輸出,這樣,垃圾收集器的行為就可以被看到並用於以後分析了。

注意,對於 J2SE 5.0 和之前的版本,我們建議 i-cms 使用如下的初始命令列引數:

這樣,就是用了和 Java SE 6 一致的引數了,多出的三個引數用於自動調整佔空比。這些多餘的引數值完全是使用的 Java SE 6 的預設值。

7.9.3 基本問題處理

i-cms 的自動佔空比計算模式使用了程式執行時收集到的統計資訊進行佔空比計算,以保證併發垃圾收集器可以在堆佔滿之前完成。不過,使用過去的行為預測將來的變化的估計方式可能並不總是足夠準確,可能在某些情況下無法阻止堆用滿。如果需要收集的垃圾太多,可以嘗試下面這些步驟,一次使用一個:

Step

Options

1. 增加保險係數

-XX:CMSIncrementalSafetyFactor=<N>

2. 增加最小佔空比

-XX:CMSIncrementalDutyCycleMin=<N>

3. 關閉自動佔空比計算,使用固定佔空比

-XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N>

7.10 測量

下面是使用-verbose:gc和-XX:+PrintGCDetails引數時,併發垃圾收集器的輸出,一些小細節已經被去掉了。注意,併發垃圾收集器的輸出裡摻雜著小回收的輸出;典型情況下,很多小回收會發生在併發收集週期之中。其中的CMS-initial-mark表徵了一個併發垃圾回收週期的開始。CMS-concurrent-mark: 標誌著併發標記階段的完成,而CMS-concurrent-sweep則標誌著併發清除階段的完成。之前沒有提到過的預清除階段以CMS-concurrent-preclean為標誌。預清除可以和重標記階段CMS-remark的準備工作同時執行。最後一個階段是CMS-concurrent-reset,這是下一個併發收集週期的準備工作。

初始標記停頓在典型情況下比小回收的停頓時間還要小。而如上例所示,併發階段(併發標記、併發預清除和併發清除)通常會比小回收長很多。不過注意,應用並沒有在這些併發階段中停頓下來。重標記停頓通常和一個小回收的長度相當。重標記停頓揮手道應用的某些特徵(如高物件修改頻率可能會增加這個停頓)和上一次小回收的時間(即,更多的年輕代物件可能會增加這個停頓)的影響。

8. 其他考慮

8.1 永久代尺寸

在大部分應用中,永久代對於垃圾回收效能沒有顯著的影響。不過,一些應用會動態的生成與載入很多類;比如,一些 JavaServer Pages(JSP)頁面的實現。這些應用可能需要很大的永久代去存放一些多餘的類。如果這樣的話,最大永久代的尺寸可以用命令列引數-XX:MaxPermSize=<N>來增大。

8.2 Finalization; Weak, Soft and Phantom References

一些應用使用 finalization 和 weak, soft, phantom 引用與垃圾收集器互動。這些特徵可以 Java 語言層帶來效能影響。一個例子是通過 finalization 來關閉檔案描述符,這會導致一個外部資源依賴於垃圾收集器。以來垃圾收集器來管理記憶體之外的資源是個壞主意。

參考資料章節中的文章深度討論了一些finalization的常見錯誤和用來避免這些錯誤的技術。

8.3 顯式垃圾回收

應用程式和垃圾回收器的另一個互動途徑是顯式呼叫 System.gc() 進行完整的垃圾回收。這回強制進行一次主回收,即使沒有必要(也就是說一次小回收可能就足夠了),所以應該避免這種情況。顯式垃圾回收對效能的影響可以通過使用 -XX:+DisableExplicitGC 進行比較來進行測量,這樣虛擬機器會無視 System.gc() 的。

最常見的顯式呼叫垃圾回收的場景是 RMI 的分散式垃圾回收。使用 RMI 的應用會引用到其他虛擬機器中的物件。在這種分散式應用的場景下,本地堆中的垃圾可能不能被回收掉,所以 RMI 會週期性強制進行完整的垃圾回收。這些回收的頻率可以使用引數來控制。如

java -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 …

這裡指定了垃圾回收每小時執行一次,而不是預設的每分鐘一次。不過,這可能會導致某些物件的清除消耗太長時間。這些引數可以被設定到高達Long.MAX_VALUE來讓顯式垃圾回收的間隔時間無限長,如果沒有合適的DGC上限時間的話。

8.4 Soft References

Soft reference在虛擬機器中比在客戶集中存活的更長一些。其清除頻率可以用命令列引數 -XX:SoftRefLRUPolicyMSPerMB=<N>來控制,這可以指定每兆堆空閒空間的 soft reference 保持存活(一旦它不強可達了)的毫秒數,這意味著每兆堆中的空閒空間中的 soft reference 會(在最後一個強引用被回收之後)存活1秒鐘。注意,這是一個近似的值,因為 soft reference 只會在垃圾回收時才會被清除,而垃圾回收並不總在發生。

8.5 Solaris 8 替換 libthread

Solaris 8 作業系統提供了一個替代的執行緒庫,libthread, 它將執行緒直接繫結成了輕量級程式(LWP)。有些應用能夠從中極大獲益,並潛在的對所有多執行緒應用都或多或少的有好處。下面的命令會為 java 啟用替換的 libthread(BASH 格式)

這個方法僅對 Solaris 8 適用,因為對 Solaris 9 作業系統來說,這是預設的,而 Solaris 10 中,這是惟一的執行緒庫。

9. 相關資源

1.HotSpot VM Frequently Asked Questions (FAQ)

2.GC output examples 介紹瞭如何解釋不同垃圾收集器的輸出。

3.How to Handle Java Finalization’s Memory-Retention Issues 介紹了一些容易犯的錯誤和避免他們的方法。

4.Richard Jones and Rafael Lins, Garbage Collection: Algorithms for Automated Dynamic Memory Management, Wiley and Sons (1996), ISBN 0-471-94148-4

名詞“Java Virtual Machine” 和“JVM” 都代表 Java 平臺虛擬機器。

來源:http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html

相關文章