1. 程式人生 > >mozi_song

mozi_song

想給專案程式碼做做調優但有許多疑惑,比如有哪些引數要調、怎麼調、使用什麼工具、調優的效果如何定量測量等。發現Oracle的這份資料不錯,簡潔直接,回答了我的許多問題,給了許多很實用的大方向上的指導。將其中精華記錄下來,希望能給同樣入門的朋友一些啟示。

Garbage Collectors

垃圾收集器 (Garbage Collectors)是JVM中的記憶體管理工具。它的職責包括:

  • 在年輕代為物件分配空間,並將存活比較久的物件移動到年老代;
  • 在堆佔用率超過某閾值時觸發concurrent marking phase,在年老代找到存活的物件;
  • 觸發parallel copying,壓縮活著的物件,釋放垃圾空間。

看起來有點抽象,並且貌似沒提到年輕代的垃圾收集,其實已經在第一條中提到了:“將存活比較久的物件移動到年老代”,這裡隱含了對年輕代進行存活物件登記和收集的過程。簡而言之,垃圾收集器的職責是:給物件分配記憶體;收集年輕代垃圾;收集年老代垃圾

串/並行Garbage Collector的選擇

一般來說,JVM會根據系統的物理配置等因素選擇一個預設的垃圾處理器。但顯然,不同的應用程式有不同的行為(如使用記憶體的頻率、物件的平均存活時間);也有不同的要求(如有介面的程式要求響應快速,而伺服器端程式要求吞吐量高,能處理更多的請求)。所以,根據不同程式的特點,可能需要不同的垃圾處理器來管理。在此處,我們先從串/並行的角度淺淺地瞭解一下這個問題。

垃圾處理器可以粗略地分為序列進行和並行進行的,即垃圾處理這個過程在單執行緒還是多執行緒中進行;在Java SE 1.4之前的版本不支援並行。根據Amdahl's law (程式能夠通過並行來加速的程度取決於程式中必須序列執行的部分),如果GC是序列進行的,則一個並行的應用程式的加速程度會受到GC的影響。假設我們通過增加處理器個數的方式來加速一個應用程式,那麼隨著處理器個數的增多,GC拖後腿的程度也越來越厲害,看下圖:

GC時間所佔的百分比隨處理器個數的變化

這是一個數學模擬圖,模擬了一個理想(完全並行)的應用程式的吞吐量受GC時間的影響。橫軸代表處理器個數,縱軸代表吞吐量,不同顏色的曲線代表GC百分比不同的程式。紅色曲線表示一條在單CPU下GC時間佔1%的程式,在處理器個數增加到32個時,GC佔整個程式執行時間的百分比竟超過了20%。

可以看到GC所佔的時間百分比越大,拖後腿的程度就越厲害。這是一個很好理解的現象,因為GC是序列的,所以其執行時間不受處理器數量的影響。隨著處理器的增多,應用程式本身的執行時間下降了,所以顯得GC所佔的時間百分比越來越大。

因此,在小系統上開發應用時可以忽略的一些GC小問題,當擴充套件到大型系統上時就會變得十分可觀,甚至成為效能瓶頸。但是,此時在垃圾處理器上做一些小文章就有可能極大地增加效能。比如考慮到上圖反映的現象,或許我們可以考慮換一個並行的垃圾處理器以提高吞吐量。

另一方面,小型應用如果不需要其他特殊的GC行為,通常使用序列垃圾處理器就夠了,選擇其他垃圾處理器可能反而會引入額外的複雜性和開銷。

分代模型

在處理垃圾時,需要先找到所有活著的物件,然後將剩下的作為垃圾進行處理。“找到所有活著的物件”這個過程需要耗費的時間與活著的物件數量成正比,這樣的話,如果應用中本來就維護了大量的存活物件,那麼找到活著的物件需要耗費大量的時間。為了優化這個過程,JVM程式設計師們基於一些經驗提出了分代收集的思想。在這些經驗中,最重要的是分代假設,即大部分物件都只存活很短的時間。

物件壽命的典型分佈圖

上圖中,橫軸代表總的位元組分配數,即時間軸;縱軸代表不同時間下存活的物件所佔位元組數。左邊的尖峰代表分配空間沒多久就可以回收的物件,比如在某個loop中臨時分配的物件,它們的壽命只有一個loop的時間;最右邊代表存活很久的那些物件,比如初始化時就存在且一直活到程式結束的物件;在這兩極之間,有一些用於中間計算的物件,即左邊的尖峰右邊的這個包。

不同應用程式的物件壽命分佈圖是不一樣的,但是許多應用的大致分佈都符合上面這個圖,這為分代收集奠定了一個很好的事實基礎:大部分物件都在年輕時死去。

比如在一個公園裡掃落葉,騰出空地讓行人行走。有一些樹掉葉子特別厲害,一小會兒地上就掉滿了;而另外一些樹每小時只掉一兩片葉子。假設清潔工為了省力,每過一段時間就清掃一次,掃完了則回到椅子上休息。如果我是清潔工,肯定會選擇集中打掃那些掉得厲害的樹,而且可能會以比較高的頻率打掃;至於那些掉得不厲害的樹,只要偶爾看一下,等落滿的時候再打掃就好了。如果每次都要把所有的樹下打掃一遍,為了照顧那些掉得厲害的樹我的打掃頻率需要很高,我會很累,而且打掃時間也會變長,效率降低。

除了序列收集器和G1之外,其他收集器預設使用以下分代模型:

預設分代模型

模型分為年輕代和年老代,年輕代分為eden和兩個survivor,virtual空間代表JVM向作業系統預訂但還未實際分配的空間。

調優指標

maximum pause time

pause time是指垃圾處理器停止應用程式的執行,專注於空間釋放時所花的時間。如果使用的是並行垃圾處理器,可以通過-XX:MaxGCPauseMillis=<nnn>這個命令列引數設定期望的最大pause time。(如果未設定,預設沒有最大時間要求)

垃圾處理器會維護每次垃圾收集pause time的均值和方差,當均值與方差的和大於設定的MaxGCPauseMillis引數時,垃圾處理器會認為停留時間目標未達到,然後調整堆的大小和其它的有關引數來試圖達到目標。

此處的maximum pause time和下面即將提到的throughput是一對相愛相殺的姐妹。通常減小堆size會優化pause time(掃描、處理時間減少),但是堆變小造成GC頻率升高,從而導致throughput下降。對於這兩者,垃圾處理器的處理方式是優先達到設定的pause time目標,其次再達到throughput目標。

throughput

吞吐量通過GC時間比例測量。 GC時間比例 = GC時間 / (GC時間 + 應用執行時間),其中的GC時間包括所有代的GC時間。如果使用的是並行垃圾處理器,吞吐量可以通過-XX:GCTimeRatio=<nnn>設定,若<nnn>為19,則GC時間比例為1/(1+19)=5%,即垃圾處理的時間佔總時間的5%。

如果GCTimeRatio未達到要求,垃圾收集器會增加年輕代和年老代的大小來降低GCTimeRatio

footprint

記憶體佔用(memory footprint)指程式執行時佔用和引用的記憶體大小。

如果前面兩個目標達到了,垃圾處理器會自動收縮堆,直至其中一個目標不再滿足(一定是throughput,因為堆變小會使停留時間變短),然後再試圖滿足這個目標。

promptness

及時性 (promptness)定義為物件死去之後到物件所佔用的記憶體可以使用之前的時間。這個指標對分散式系統通常比較重要。

一般調優策略

  1. 如果堆已經達到maximum heap size但throughput目標還未達到,說明設定的maximum heap size太小,可以嘗試將其設為接近實體記憶體但還不至於導致記憶體交換的值。如果還是達不到throughput目標,說明這個throughput目標對於當前平臺上的記憶體大小來說過高了。
  2. 如果throughput目標已經滿足,但停留時間過長,則可以增加maximum pause time目標。但這樣throughput目標有可能又得不到滿足了,此時需要根據自己的判斷作一個折中。
  3. 如上文所說,throughput與pause time對堆大小的要求相反,是一對相互競爭的指標。它們之間的相互競爭可能造成的結果是:即使應用程式已經在穩定運行了,堆大小仍然在上下振動。這表明垃圾處理器努力在兩者之間尋找一個平衡。

度量

以上所說的throughput等指標需要根據應用的不同特性去測量。比如要測一個web server的throughput,可以用一個自己寫的client load generator;測試Solaris系統上伺服器的記憶體佔用,可以用pmap這個命令;若要測GC的停留時間,則可以通過命令列引數-verbose:gc直接觀察JVM的診斷輸出。

總結

本文定性介紹了GC調優的一些初級概念,為實際調優奠定基礎。但僅有這些模糊概念是遠遠不夠的,在理論和實踐上還會作出其它總結,歡迎關注。

參考資料/推薦閱讀

深入理解Java虛擬機器 推薦看第4-5章,詳細講解了調優工具的使用以及幾個調優例項

高質量Java程式設計 推薦看第3章,作者用一個xml parser的例子給出了調優實戰講解,可惜本書不再再版,也未找到電子版