JVM調優實戰
文檔修訂記錄
| 版本 |
日期 |
撰寫人 |
審核人 |
批准人 |
變更摘要 & 修訂位置 |
目錄
-
理論篇
-
多功能養魚塘-JVM內存
大魚塘O(可分配內存): JVM可以調度使用的總的內存數,這個數量受操作系統進程尋址范圍、系統虛擬內存總數、系統物理內存總數、其他系統運行所占用的內存資源等因素的制約。
小池塘A(堆內存):JVM運行時數據區域,它為類實例和數組分配的內存。堆可以是固定大小的也可以是可變大小的。其中 Heap = {Old + NEW = { Eden , from, to } }。
小池塘B(非堆內存):包括所有線程之間共享的一個方法區域和JVM為優化或內部處理所分配的內存。它存儲每一個類的結構,如一個運行時的常量池、字段和方法數據、方法的代碼和構造函數。這個方法區是邏輯上堆的一部分,但依賴於實現,一個JVM可以不去回收或者壓縮它。像堆一樣,方法區可以固定大小的,也可以是大小可變的。方法區不是必須是連續的,它們可以是不連續的。除方法區之外,JVM總是從非堆中分配用於優化和內部處理所需的內存。例如,JIT編譯器為高性能的JVM代碼轉換存儲成本地代碼而分配的內存。
整個池塘結構圖如下:

查看大池塘O大小的方法為:
在命令行下用 java -XmxXXXXM -version 命令來進行測試,然后逐漸的增大XXXX的值,如果執行正常就表示指定的內存大小可用,否則會打印錯誤信息,示例如下:
java -Xmx3072M -version。
當一個URL被訪問時,內存申請過程如下:
A. JVM會試圖為相關Java對象在Eden中初始化一塊內存區域
B. 當Eden空間足夠時,內存申請結束。否則到下一步
C. JVM試圖釋放在Eden中所有不活躍的對象(這屬於1或更高級的垃圾回收), 釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區
D. Survivor區被用來作為Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區
E. 當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集(0級)
F. 完全垃圾收集后,若Survivor及OLD區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現"out of memory錯誤"
-
池塘中的魚-程序中的對象
程序中運行的各種類實例稱之為對象,每個對象都有不同的生命周期,有的存活時間長點,有的存活時間短點,這就想魚塘中養的不同生長期的魚一樣,有的三個月就可以上市,有的魚則需要6個月甚至更長的時間才能上市。JVM內存機制的設置就是為了要滿足這種不同生命周期的對象對內存的需求,並使之能達到最大的性能表現。
-
養殖區域划分-JVM中的代
魚塘主人為了充分利用現有的條件來賺取更多的利潤,他需要喂養各種不同種類的魚,於是又把魚塘分割成了幾塊不同區域:"魚苗養殖區"、"短中期養殖區"、"長期養殖區",來養殖不同生長周期的魚。JVM同樣為了對各種不同生命周期的對象進行有效管理也划分了各種不同的區域,這就是"代"的概念,分別叫做:"青年代"、"老年代"、"持久代",下面逐一介紹每個代的含義和作用。
短中期魚苗養殖區-年青代(Young Generation)
年青代由一個Eden Space和兩個Survivor Spaces組成,虛擬機初始時分配所有的對象到Eden Space,許多對象也是在這里死去。當它執行一個"minor GC"的時候,虛擬機將從Eden Space中移動一些殘余的對象到其中的一個Survivor Spaces中。青年代就好像養魚塘中的"中短期養殖區"一樣,主人把魚先投放到"短期養殖區"喂養,隔一段時間就開始下網撈出已經長成的那些魚拿到集市去賣,這個過程就是從"Eden Space"中執行垃圾回收的過程。主人接着把捕撈之后剩下的"漏網之魚"趕到"中期養殖區"繼續喂養。這個"中期養殖區"就是"Survivor Spaces",當然魚在"中期養殖區"喂養一段時間后也要撈出那些長成的魚去賣,這就是對"Survivor Spaces" 執行垃圾回收的過程。
Ps Eden Space: 這個內存池在對象初始化時被分配;
Ps Survivor Space: 這個內存池中包含着Eden Space 經過GC之后幸存下來的對象;
年輕代設置策略:對於響應時間優先的應用需盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達老年代的對象。對於吞吐量優先的應用則盡可能的設置大,可達到Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
長期養殖區-老年代(老年代):、
虛擬機將在Survivor Spaces中生存足夠長時間的對象移動到老年代的Tenured Spaces中。當Tenured Generation被填滿,則將執行一個完全GC,這個完全GC非常的慢,因為它要處理所有存活着的對象,用的是串行標記收集的方式,並發收集可以減少對於應用的影響。
老年代設置策略:對於響應時間優先的應用,老年代使用並發收集器,所以其大小需要小心設置,一般要考慮並發會話率和會話持續時間等一些參數。如果堆設置小了,可能會造成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;如果堆大了,則需要較長的收集時間。
最優化的方案,一般需要參考以下數據獲得:
-
並發垃圾收集信息
-
持久代並發收集次數
-
傳統GC信息
-
花在年輕代和老年代回收上的時間比例
-
減少年輕代和老年代花費的時間,一般會提高應用的效率
對於吞吐量優先的應用,一般吞吐量優先的應用都有一個很大的年輕代和一個較小的老年代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而老年代盡存放長期存活對象。
較小堆引起的碎片問題 :因為老年代的並發收集器使用標記、清除算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合並,這樣可以分配給較大的對象。但是,當堆空間較小時,運行一段時間以后,就會出現"碎片",如果並發收集器找不到足夠的空間,那么並發收集器將會停止,然后使用傳統的標記、清除方式進行回收。如果出現"碎片",可能需要進行如下配置: -XX:+UseCMSCompactAtFullCollection 使用並發收集器時,開啟對老年代的壓縮;-XX:CMSFullGCsBeforeCompaction=0上面配置開啟的情況下,這里設置多少次Full GC后,對老年代進行壓縮。
/*監控實例*/
內存池名稱: Tenured Gen
Java 虛擬機最初向操作系統請求的內存量: 3,538,944 字節
Java 虛擬機實際能從操作系統獲得的內存量: 1,431,699,456 字節
Java 虛擬機可從操作系統獲得的最大內存量: 1,431,699,456 字節。請注意,並不一定能獲得該內存量。
Java 虛擬機此時使用的內存量: 1,408,650,472 字節
/*監控實例*/
實例說明:系統能獲得的最大Tenured Generation空間大小為1.431G左右,此時使用已經1.408G,基本滿了,所以在JVM執行串行標記垃圾收集時,系統響應速度會很慢!
魚苗養殖區-持久代(Permanent Generation)
控制着所有虛擬機自己映射的數據,如類和對象的方法。在持久代中jvm則存儲class和method對象。持久代就像魚苗養殖區一樣,池塘主人一次對該區域投入足夠量的魚苗,已保證其他魚塘的足夠供應。就配置而言,永久域是一個獨立域並且不認為是堆的一部分。永久域默認大小為4m.運行程序時,jvm會調整永久域的大小以滿足需要。每次調整時,jvm會對堆進行一次完全的垃圾收集。 使用-XX:MaxPerSize標志來增加永久域搭大小。在WebLogic Server應用程序加載較多類時,經常需要增加永久域的最大值。當jvm加載類時,永久域中的對象急劇增加,從而使jvm不斷調整永久域大小。為了避免調整,可使用-XX:PerSize標志設置初始值。
-
主人定期捕魚-JVM垃圾回收
一個池塘收容積限制,能養殖的魚的數量是一定的,因此隔一段時間必須撈出部分長成的魚來使主人能喂養很多的魚。同樣,JVM所管理的有限內存也要實現最優化利用,Garbage Collection(GC)就是用來釋放沒有被引用的對象所占領的內存,目的在於清除不再使用的對象。GC通過算法和參數的配置可以對性能產生效果顯著的影響。
GC就好像把長成的魚從池塘中撈出來拿到市場上去賣,然后給池塘騰出空間繼續養別的魚賺錢。采用哪種養殖方式能讓魚塘主人賺到更大的利潤是魚塘主人的經營目的,而JVM調優的目的在於如何能是系統表現出更好的響應時間、更大的吞吐量。
Minor Collections(局部垃圾回收):當通用內存消耗完被分配的內存時,JVM會在內存池上執行一個局部的GC(總是調用minor collection)去釋放被dead的對象所占用的內存。這個局部的GC通常比完全GC要快許多。青年代中的垃圾回收就是采用局部垃圾回收機制,因此,青年代中內存分配和管理效率也是最高。
通常情況下,對於內存的申請優先在青年代中申請,當內存不夠時會整理新生代,當整理以后還是不能滿足申請的內存,就會向老年代移動一些生命周期較長的對象。這種整理和移動會消耗資源,同時降低系統運行響應能力,因此如果青年代設置的過小,就會頻繁的整理和移動,對性能造成影響。那是否把年青代設置的越大越好,其實不然,青年代采用的是復制搜集算法,這種算法必須停止所有應用程序線程,服務器線程切換時間就會成為應用響應的瓶頸。
Major Collections(完全垃圾回收):當老年代需要被回收,這就是一個major collection ,它的運行常常非常慢,因為它要涉及所有存活着的類。
/*實例*/
垃圾收集器的名稱: Copy
使用此垃圾收集器收集的數量: 219 字節
垃圾收集時間: 18 秒 630 毫秒
垃圾收集器的名稱: MarkSweepCompact
使用此垃圾收集器收集的數量: 47 字節
垃圾收集時間: 36 秒 166 毫秒
實例說明:copy垃圾搜集器的運行時間為18秒回收219字節,回收速度為平均每秒12字節,而MKC垃圾搜集器的時間為36秒回收了47字節,回收速度為平均每秒1.3字節,兩者差距幾乎達到了10倍,可見完全垃圾回收的速度遠不如局部垃圾回收。
-
不同的捕魚方式-垃圾回收器
Sun JVM提供有4垃圾回收器:
Serial Collector(序列垃圾回收器):垃圾回收器對Young Gen和Tenured Gen都是使用單線的垃圾回收方式,對Young Gen,會使用拷貝策略避免內存碎片,對Old Gen,會使用壓縮策略避免內存碎片。在JVM啟動參數中使用-XX:+UseSerialGC啟用Serial Collector。串行收集器只適用於小數據量的情況,默認情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應參數。基本上在多內核的服務器上應該避免使用這種方式。JDK5.0以后,JVM會根據當前系統配置進行判斷。串行GC適合小型應用和單處理器系統(無需多線程交互,效率比較高)。
Parallel Collector(並發垃圾回收器):垃圾回收器對Young Gen和Tenured Gen都是使用多線程並行垃圾回收的方式,對Young Gen,會使用拷貝策略避免內存碎片,對Old Gen,會使用壓縮策略避免內存碎片。在JVM啟動參數中使用-XX:+UseParallelGC啟用Parallel Collector。這是一種吞吐量優先的並行收集器 ,主要以到達一定的吞吐量為目標,適用於科學技術和后台處理等。采用了多線程並行管理和回收垃圾對象,提高了回收效率和服務器的吞吐量,適合於多處理器的服務器。
Parallel Compacting Collector(並行壓縮垃圾回收器):與Parallel Collector垃圾回收類似,但對Tenured Gen會使用一種更有效的垃圾回收策略,此垃圾回收器在暫停時間上會更短。在JVM啟動參數中使用-XX:+UseParallelOldGC啟用Parallel Compacting Collector。這是一種響應時間優先的並發收集器 ,主要是保證系統的響應時間,減少垃圾收集時的停頓時間。適用於應用服務器、電信領域等。
Concurrent Mark-Sweep (CMS) Collector(並發標志清除垃圾回收器):對Young Gen會使用與Parallel Collector同樣的垃圾回收策略,對Tenured Gen垃圾回收的垃圾標志線程與應用線程同時進行,而垃圾清除則需要暫停應用線程,但暫停時間會大大縮減,需要注意的是,由於垃圾回收過程更加復雜,會降低總體的吞吐量。
這里說一下並行和並發的區別,並行指的是多個進程並行執行垃圾回收,那么可以很好的利用多處理器,而並發指的是應用程序不需要暫停可以和垃圾回收線程並發工作。
說明:對於聯機處理的應用系統或復雜的3層應用系統,采用Concurrent Mark-Sweep (CMS) Collector進行垃圾搜集,基本上既能保證性能,又能保證穩定性(暫停時間短)。
-
捕魚工具選擇-JVM參數
-
通用JVM參數
-server
如果不配置該參數,JVM會根據應用服務器硬件配置自動選擇不同模式,server模式啟動比較慢,但是運行期速度得到了優化,適合於服務器端運行的JVM。
-client
啟動比較快,但是運行期響應沒有server模式的優化,適合於個人PC的服務開發和測試。
-Xmx
設置java heap的最大值,默認是機器物理內存的1/4。這個值決定了最多可用的Java堆內存:分配過少就會在應用中需要大量內存作緩存或者臨時對象時出現OOM(Out Of Memory)的問題;如果分配過大,那么就會因PermSize過小而引起的另外一種Out Of Memory。所以如何配置還是根據運行過程中的分析和計算來確定,如果不能確定還是采用默認的配置。
-Xms
設置Java堆初始化時的大小,默認情況是機器物理內存的1/64。這個主要是根據應用啟動時消耗的資源決定,分配少了申請起來會降低運行速度,分配多了也浪費。
-XX:PermSize
初始化永久內存區域大小。永久內存區域全稱是Permanent Generation space,是指內存的永久保存區域,程序運行期不對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤。這種錯誤常見在web服務器對JSP進行pre compile的時候。 如果你的WEB APP下用了大量的第三方jar,其大小超過了jvm默認的PermSize大小(4M)那么就會產生此錯誤信息了。
-XX:MaxPermSize
設置永久內存區域最大大小。
-Xmn
直接設置青年代大小。整個JVM可用內存大小=青年代大小 + 老年代大小 + 持久代大小 。持久代一般固定大小為64m,所以增大年輕代后,將會減小老年代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
按照Sun的官方設置比例,則上面的例子中年輕代的大小應該為2048*3/8=768M。
-XX:NewRatio
控制默認的Young代的大小,例如,設置-XX:NewRatio=3意味着Young代和老年代的比率是1:3。換句話說,Eden和Survivor空間總和是整個堆大小的1/4。

如圖中的實際設置,-XX:NewRatio=2,-Xmx=2048,則年輕代和老年代的分配比例為1:2,即年輕代的大小為682M,而老年代的大小為1365M。查看實際系統的jvm監控結果為:
內存池名稱: Tenured Gen
Java 虛擬機最初向操作系統請求的內存量: 3,538,944 字節
Java 虛擬機實際能從操作系統獲得的內存量: 1,431,699,456 字節
Java 虛擬機可從操作系統獲得的最大內存量: 1,431,699,456 字節。請注意,並不一定能獲得該內存量。
Java 虛擬機此時使用的內存量: 1,408,650,472 字節
即:1,408,650,472 字節=1365M,證明了上面的計算是正確的。
-XX:SurvivorRatio
設置年輕代中Eden區與Survivor區的大小比值。設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6。越大的survivor空間可以允許短期對象盡量在年青代消亡;如果Survivor空間太小,Copying收集將直接將其轉移到老年代中,這將加快老年代的空間使用速度,引發頻繁的完全垃圾回收。
如下圖:

SurvivorRatio的值設為3,Xmn為768M,則每個Survivor空間的大小為768M/5=153.6M。
-XX:NewSize
為了實現更好的性能,您應該對包含短期存活對象的池的大小進行設置,以使該池中的對象的存活時間不會超過一個垃圾回收循環。新生成的池的大小由 NewSize 和 MaxNewSize 參數確定。通過這個選項可以設置Java新對象生產堆內存。在通常情況下這個選項的數值為1024的整數倍並且大於1MB。這個值的取值規則為,一般情況下這個值-XX:NewSize是最大堆內存(maximum heap size)的四分之一。增加這個選項值的大小是為了增大較大數量的短生命周期對象。增加Java新對象生產堆內存相當於增加了處理器的數目。並且可以並行地分配內存,但是請注意內存的垃圾回收卻是不可以並行處理的。作用跟-XX:NewRatio相似, -XX:NewRatio是設置比例而-XX:NewSize是設置精確的數值。
-XX:MaxNewSize
通過這個選項可以設置最大Java新對象生產堆內存。通常情況下這個選項的數值為1 024的整數倍並且大於1MB,其功用與上面的設置新對象生產堆內存-XX:NewSize相同。一般要將NewSize和MaxNewSize設成一致。
-XX:MaxTenuringThreshold
設置垃圾最大年齡。如果設置為0的話,則年輕代對象不經過Survivor區,直接進入老年代。對於老年代比較多的應用,可以提高效率。如果將此值設置為一個較大值,則年輕代對象會在Survivor區進行多次復制,這樣可以增加對象在年輕代的存活時間,增加在年輕代即被回收的概率。
如下圖:

-XX:MaxTenuringThreshold參數被設置成5,表示對象會在Survivor區進行5次復制后如果還沒有被回收才會被復制到老年代。
-XX:GCTimeRatio
設置垃圾回收時間占程序運行時間的百分比。該參數設置為n的話,則垃圾回收時間占程序運行時間百分比的公式為1/(1+n) ,如果n=19表示java可以用5%的時間來做垃圾回收,1/(1+19)=1/20=5%。
-XX:TargetsurvivorRatio
該值是一個百分比,控制允許使用的救助空間的比例,默認值是50。該參數設置較大的話可提高對survivor空間的使用率。當較大的堆棧使用較低的SurvivorRatio時,應增加該值到80至90,以更好利用救助空間。
-Xss
設置每個線程的堆棧大小,根據應用的線程所需內存大小進行調整,在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。當這個選項被設置的較大(>2MB)時將會在很大程度上降低系統的性能。因此在設置這個值時應該格外小心,調整后要注意觀察系統的性能,不斷調整以期達到最優。
JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。
-Xnoclassgc
這個選項用來取消系統對特定類的垃圾回收。它可以防止當這個類的所有引用丟失之后,這個類仍被引用時不會再一次被重新裝載,因此這個選項將增大系統堆內存的空間。禁用類垃圾回收,性能會高一點;
-
串行收集器參數
-XX:+UseSerialGC:
設置串行收集器 。
-
並行收集器參數
-XX:+UseParallelGC:
選擇垃圾收集器為並行收集器,此配置僅對年輕代有效,即上述配置下,年輕代使用並行收集,而老年代仍舊使用串行收集。采用了多線程並行管理和回收垃圾對象,提高了回收效率,提高了服務器的吞吐量,適合於多處理器的服務器。
-XX:ParallelGCThreads
配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。
-XX:+UseParallelOldGC:
采用對於老年代並發收集的策略,可以提高收集效率。JDK6.0支持對老年代並行收集。
-XX:MaxGCPauseMillis
設置每次年輕代並行收集最大暫停時間,如果無法滿足此時間,JVM會自動調整年輕代大小以滿足此值。
-XX:+UseAdaptiveSizePolicy:
設置此選項后,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低響應時間或者收集頻率等,此值建議使用並行收集器時,一直打開。
-
並發收集器參數
-XX:+UseConcMarkSweepGC
指定在 老年代 使用 concurrent cmark sweep gc。gc thread 和 app thread 並行 ( 在 init-mark 和 remark 時 pause app thread)。app pause 時間較短 , 適合交互性強的系統 , 如 web server。它可以並發執行收集操作,降低應用停止時間,同時它也是並行處理模式,可以有效地利用多處理器的系統的多進程處理。
-XX:+UseParNewGC
指定在 New Generation 使用 parallel collector, 是 UseParallelGC 的 gc 的升級版本 , 有更好的性能或者優點 , 可以和 CMS gc 一起使用
-XX:+UseCMSCompactAtFullCollection:
打開對老年代的壓縮。可能會影響性能,但是可以消除碎片,在FULL GC的時候, 壓縮內存, CMS是不會移動內存的, 因此, 這個非常容易產生碎片, 導致內存不夠用, 因此, 內存的壓縮這個時候就會被啟用。 增加這個參數是個好習慣。
-XX:+CMSIncrementalMode:
設置為增量模式。適用於單CPU情況
-XX:CMSFullGCsBeforeCompaction
由於並發收集器不對內存空間進行壓縮、整理,所以運行一段時間以后會產生"碎片",使得運行效率降低。此值設置運行多少次GC以后對內存空間進行壓縮、整理。
-XX:+CMSClassUnloadingEnabled
使CMS收集持久代的類,而不是fullgc
-XX:+CMSPermGenSweepingEnabled
使CMS收集持久代的類,而不是fullgc。
-XX:-CMSParallelRemarkEnabled
在使用 UseParNewGC 的情況下 , 盡量減少 mark 的時間。
-XX:CMSInitiatingOccupancyFraction
說明老年代到百分之多少滿的時候開始執行對老年代的並發垃圾回收(CMS),這個參數設置有很大技巧,基本上滿足公式:
(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn
時就不會出現promotion failed。在我的應用中Xmx是6000,Xmn是500,那么Xmx-Xmn是5500兆,也就是老年代有5500兆,CMSInitiatingOccupancyFraction=90說明老年代到90%滿的時候開始執行對老年代的並發垃圾回收(CMS),這時還剩10%的空間是5500*10%=550兆,所以即使Xmn(也就是年輕代共500兆)里所有對象都搬到老年代里,550兆的空間也足夠了,所以只要滿足上面的公式,就不會出現垃圾回收時的promotion failed;
如果按照Xmx=2048,Xmn=768的比例計算,則CMSInitiatingOccupancyFraction的值不能超過40,否則就容易出現垃圾回收時的promotion failed。
-XX:+UseCMSInitiatingOccupancyOnly
指示只有在老年代在使用了初始化的比例后 concurrent collector 啟動收集
-XX:SoftRefLRUPolicyMSPerMB
相對於客戶端模式的虛擬機(-client選項),當使用服務器模式的虛擬機時(-server選項),對於軟引用(soft reference)的清理力度要稍微差一些。可以通過增大-XX:SoftRefLRUPolicyMSPerMB來降低收集頻率。默認值是 1000,也就是說每秒一兆字節。Soft reference在虛擬機中比在客戶集中存活的更長一些。其清除頻率可以用命令行參數 -XX:SoftRefLRUPolicyMSPerMB=<N> 來控制,這可以指定每兆堆空閑空間的 soft reference 保持存活(一旦它不強可達了)的毫秒數,這意味着每兆堆中的空閑空間中的 soft reference 會(在最后一個強引用被回收之后)存活1秒鍾。注意,這是一個近似的值,因為 soft reference 只會在垃圾回收時才會被清除,而垃圾回收並不總在發生。
-XX:LargePageSizeInBytes
內存頁的大小, 不可設置過大,會影響Perm的大小。
-XX:+UseFastAccessorMethods
原始類型的快速優化,get,set 方法轉成本地代碼。
-XX:+DisableExplicitGC
禁止 java 程序中的 full gc, 如 System.gc() 的調用。 最好加上防止程序在代碼里誤用了,對性能造成沖擊。
-XX:+AggressiveHeap
特別說明下:(我感覺對於做java cache應用有幫助)
試圖是使用大量的物理內存
長時間大內存使用的優化,能檢查計算資源(內存, 處理器數量)
至少需要256MB內存
大量的CPU/內存, (在1.4.1在4CPU的機器上已經顯示有提升)
-XX:+AggressiveOpts
加快編譯
-XX:+UseBiasedLocking
鎖機制的性能改善。
-
實戰篇
-
測試目的
-
測試被測系統使用不同的垃圾回收方案時的性能表現;
了解各種JVM參數在性能調優時的實際效果;
對遴選出的最優方案進行8小時壓力測試並記錄測試結果;
-
測試環境准備
被測程序的運行的軟硬件環境:
-
D630 4G內存+T7250雙核CPU+160G硬盤;
-
操作系統:windowsXP SP3;
-
IP:11.55.15.51;
被測程序名稱:
-
XXX銀行采購管理系統V1.1版;
程序部署環境:
-
Tomcat6.0.18 for windows;
-
Sun JDK1.6.13 for windows;
-
Oracle10g for windows(單獨運行在另外一台640M筆記本上)
性能測試工具運行的軟硬件環境:
-
操作系統:windowsxp sp3
-
瀏覽器版本:IE7
-
IP地址:11.55.15.141
-
性能測試工具:loadrunner9.10
JVM監控工具:
-
使用Jconsole進行圖形化監控;
確定JVM在被測系統的機器上最大可用內存:
通過在命令行下用 java -XmxXXXXM -version 命令反復測試發現在11.55.15.51機器上JVM能使用的最大內存為1592M。

-
錄制測試腳本
錄制前准備:修改checkcode.java文件,將隨機生成的校驗碼改成一個固定的校驗碼方便腳本的自動運行,然后將編譯好的checkcode.class文件替換發布包中的class文件。
選擇典型業務操作進行腳本錄制,每個系統的典型業務操作都會不同,需要經過分析統計,選擇用戶操作頻率最高的部分。經過分析后確定的腳本內容為:錄制系統登錄操作並在登錄成功后的主界面上選取一段文字作為驗證點。
啟動VuGen程序按腳本定義內容進行錄制並調試腳本,保證腳本能正常運行。
-
定義測試場景
-
虛擬用戶數:30
-
持續運行時間:8小時
-
虛擬用戶加載和卸載方式:同時
-
性能監控指標:響應時間、吞吐量、成功交易數
-
執行初步性能測試
使用系統默認的參數執行測試,並記錄響應時間、吞吐量已經成功交易數等數據,同時監控JVM的使用情況。
-
選擇調優方案
不同垃圾回收方法測試數據:
| Id |
NewRatio |
SurviorRatio |
TransResponse Time |
Throughput |
Passed Transactions |
| 1 |
2 |
25 |
3.139s |
3016230.514 |
7528 |
| 2 |
1 |
25 |
3.161s |
2975581.301 |
7452 |
| 3 |
3 |
25 |
2.814s |
3334717.818 |
8383 |
| 4 |
4 |
25 |
2.659s |
3505592.450 |
8846 |
| 5 |
5 |
25 |
2.860s |
3270596.069 |
8232 |
| 6 |
4 |
15 |
2.499s |
3765121.986 |
9426 |
| 7 |
4 |
5 |
1.986s |
4750776.581 |
11843 |
| 8 |
4 |
4 |
1.968s |
4825608.161 |
11947 |
| 9 |
4 |
3 |
2.507s |
3770420.243 |
9388 |
| 10 |
-XX:TargetSurvivorRatio=90 |
1.924 |
4945053.874 |
12216 |
|
| 11 |
-Xmx1024M |
1.903 |
4974137.908 |
12360 |
|
並發收集模式,運行時間十分鍾后的對內存使用情況:

串行收集模式,運行時間十分鍾:

並行收集模式,運行時間十分鍾:

30-60:30個並發用戶連續運行60分鍾的jvm內存變化截圖:

在11:36和11:56分發生了兩次完全GC(Full GC),因為這時PS Old Gen已經滿了,JVM自動對Old Gen中的內存進行了回收。
根據反復的測試並結合被測系統業務特點,最終敲定了使用以下最優方案進行8小時壓力測試:
JAVA_OPTS=-server
-Xms1024M
-Xmx1024M
-Xmn128M
-XX:NewSize=128M
-XX:MaxNewSize=128M
-XX:SurvivorRatio=20
-XX:MaxTenuringThreshold=10
-XX:GCTimeRatio=19
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
–XX:PermSize=256m
-XX:MaxPermSize=256m
-Djava.awt.headless=true
-
調優后JVM監控圖
30Vusers運行8小時截圖:







-
測試結果分析
對於XX銀行采購系統的登錄操作來說,將jvm的NewRatio 和SurviorRatio設置成4時,性能表現最好!在此基礎上在設置-XX:TargetSurvivorRatio=90和-Xmx1024M后性能也有一定程度的提升。
-
性能問題舉例
-
性能症狀
-
XX省一個正式上線運行的系統,每運行一段時間后程序進程會莫名其妙地被kill掉,不得不手工啟動系統。
-
監控結果
-
jmap命令查看堆內存分配和使用情況
./jmap -heap 31 //31為程序的進程號
Attaching to process ID 31, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0-b12 //顯示jvm的版本號
using parallel threads in the new generation. //說明在年輕代使用了並行收集
using thread-local object allocation.
Concurrent Mark-Sweep GC //啟用CMS收集模式
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70 //這兩項說明堆內存的使用比例在30%~60%之間
MaxHeapSize = 2147483648 (2048.0MB) //最大堆大小為2048M
NewSize = 805306368 (768.0MB)
MaxNewSize = 805306368 (768.0MB) //年輕代大小為768M
OldSize = 1342177280 (1280.0MB) //老年代代大小為1280M
NewRatio = 8 //這個有點自相矛盾,1:8
SurvivorRatio = 3 //救助區大小占整個年輕代的五分之一
PermSize = 268435456 (256.0MB) //持久代大小為256M
MaxPermSize = 268435456 (256.0MB) //持久代大小為256M
Heap Usage:
//年輕代大小,這里只計算了一個救助區,所以少了153M
New Generation (Eden + 1 Survivor Space):
capacity = 644284416 (614.4375MB)
used = 362446760 (345.65616607666016MB)
free = 281837656 (268.78133392333984MB)
56.25570803810968% used
//Eden Space大小為614.43-153=460.8M
Eden Space:
capacity = 483262464 (460.875MB)
used = 342975440 (327.0868682861328MB)
free = 140287024 (133.7881317138672MB)
70.97084204743864% used
//兩個救助區的大小均為153MB, 與前面的SurvivorRatio參數設置值計算結果一致。
From Space:
capacity = 161021952 (153.5625MB)
used = 19471320 (18.569297790527344MB)
free = 141550632 (134.99320220947266MB)
12.092338813530219% used
To Space:
capacity = 161021952 (153.5625MB)
used = 0 (0.0MB)
free = 161021952 (153.5625MB)
0.0% used
//老年代大小為1280M,和根據參數配置計算的結果一致。
concurrent mark-sweep generation:
capacity = 1342177280 (1280.0MB)
used = 763110504 (727.7588882446289MB)
free = 579066776 (552.2411117553711MB)
56.85616314411163% used
//永久代大小為256M,實際使用不到50%。可在系統運行一段時間后穩定該值。
Perm Generation:
capacity = 268435456 (256.0MB)
used = 118994736 (113.48222351074219MB)
free = 149440720 (142.5177764892578MB)
44.32899355888367% used
-
Top命令監控結果:

通過使用top命令進行持續監控發現此時CPU空閑比例為85.7%,剩余物理內存為3619M,虛擬內存8G未使用。持續的監控結果顯示進程29003占用系統內存不斷在增加,已經快得到最大值。
-
Jstat命令監控結果:

使用jstat命令對PID為29003的進程進行gc回收情況檢查,發現由於Old段的內存使用量已經超過了設定的80%的警戒線,導致系統每隔一兩秒就進行一次FGC,FGC的次數明顯多余YGC的次數,但是每次FGC后old的內存占用比例卻沒有明顯變化—系統嘗試進行FGC也不能有效地回收這部分對象所占內存。同時也說明年輕代的參數配置可能有問題,導致大部分對象都不得不放到老年代來進行FGC操作,這個或許跟系統配置的會話失效時間過長有關。
-
Jstack打印出的堆棧內容:

在上圖中發現大量的的工作流線程鎖定。

在上圖中發現大量的的cms線程池管理線程鎖定。
-
原因分析
通過對jvm內存進行實時監控后發現導致老年代內存不能有效回收的原因就在於堆棧中存在大量的線程死鎖問題。建議開發組認真審查com.zzxy.workflow包的源代碼以及com.web.csm包中的源代碼,看看是否存在線程死鎖的缺陷。
-
該系統的JVM設置
<jvm-options>-XX:+PrintGCApplicationConcurrentTime</jvm-options> <jvm-options>-XX:+PrintGCApplicationStoppedTime</jvm-options>
<jvm-options>-XX:+PrintGCTimeStamps</jvm-options>
<jvm-options>-XX:+PrintGCDetails</jvm-options>
<jvm-options>-Xms2048m</jvm-options>
<jvm-options>-Xmx2048m</jvm-options>
<jvm-options>-server</jvm-options>
<jvm-options>-Djava.awt.headless=true</jvm-options>
<jvm-options>-XX:PermSize=256m</jvm-options>
<jvm-options>-XX:MaxPermSize=256m</jvm-options>
<jvm-options>-XX:+DisableExplicitGC</jvm-options>
<jvm-options>-Xmn768M</jvm-options>
<jvm-options>-XX:SurvivorRatio=3</jvm-options>
<jvm-options>-Xss128K</jvm-options>
<jvm-options>-XX:TargetSurvivorRatio=80</jvm-options>
<jvm-options>-XX:MaxTenuringThreshold=5</jvm-options>
<jvm-options>-XX:+UseConcMarkSweepGC</jvm-options>
<jvm-options>-XX:+CMSClassUnloadingEnabled</jvm-options>
<jvm-options>-XX:+UseCMSCompactAtFullCollection</jvm-options>
<jvm-options>-XX:-CMSParallelRemarkEnabled</jvm-options>
-
后記
1、性能調優要做到有的放矢,根據實際業務系統的特點,以一定時間的JVM日志記錄為依據,進行有針對性的調整、比較和觀察。
2、性能調優是個無止境的過程,要綜合權衡調優成本和更換硬件成本的大小,使用最經濟的手段達到最好的效果。
3、性能調優不僅僅包括JVM的調優,還有服務器硬件配置、操作系統參數、中間件線程池、數據庫連接池、數據庫本身參數以及具體的數據庫表、索引、分區等的調整和優化。
4、通過特定工具檢查代碼中存在的性能問題並加以修正是一種比較經濟快捷的調優方法。
-
附:舍得網的典型配置
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT
-server
-Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-XX:SurvivorRatio=65536
-XX:MaxTenuringThreshold=0
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=90
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log ";
說明:
1、 -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0就是去掉了救助空間;
2、-Xnoclassgc禁用類垃圾回收,性能會高一點;
3、-XX:+DisableExplicitGC禁止System.gc(),免得程序員誤調用gc方法影響性能;
4、-XX:+UseParNewGC,對年輕代采用多線程並行回收,這樣收得快;
