想給項目代碼做做調優但有許多疑惑,比如有哪些參數要調、怎么調、使用什么工具、調優的效果如何定量測量等。發現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)定義為對象死去之后到對象所占用的內存可以使用之前的時間。這個指標對分布式系統通常比較重要。
一般調優策略
- 如果堆已經達到maximum heap size但throughput目標還未達到,說明設置的maximum heap size太小,可以嘗試將其設為接近物理內存但還不至於導致內存交換的值。如果還是達不到throughput目標,說明這個throughput目標對於當前平台上的內存大小來說過高了。
- 如果throughput目標已經滿足,但停留時間過長,則可以增加maximum pause time目標。但這樣throughput目標有可能又得不到滿足了,此時需要根據自己的判斷作一個折中。
- 如上文所說,throughput與pause time對堆大小的要求相反,是一對相互競爭的指標。它們之間的相互競爭可能造成的結果是:即使應用程序已經在穩定運行了,堆大小仍然在上下振動。這表明垃圾處理器努力在兩者之間尋找一個平衡。
度量
以上所說的throughput等指標需要根據應用的不同特性去測量。比如要測一個web server的throughput,可以用一個自己寫的client load generator;測試Solaris系統上服務器的內存占用,可以用pmap這個命令;若要測GC的停留時間,則可以通過命令行參數-verbose:gc直接觀察JVM的診斷輸出。
總結
本文定性介紹了GC調優的一些初級概念,為實際調優奠定基礎。但僅有這些模糊概念是遠遠不夠的,在理論和實踐上還會作出其它總結,歡迎關注。
參考資料/推薦閱讀
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide 文中提到的資料,Oracle寫的,具有官方指導意義
深入理解Java虛擬機 推薦看第4-5章,詳細講解了調優工具的使用以及幾個調優實例
高質量Java程序設計 推薦看第3章,作者用一個xml parser的例子給出了調優實戰講解,可惜本書不再再版,也未找到電子版
