JVM GC調優一則--增大Eden Space提高性能
緣起
線上有Tomcat升級到7.0.52版,然後有應用的JVM FullGC變頻繁,在高峰期socket連接數,Cpu使用率都暴增。
思路
思路是Tomcat本身的代碼應該是沒有問題的,有問題的可能是應用代碼升級,或者環境改變了,總之Tomcat的優先級排在最後。
先把應用的heap dump下來分析下:
jmap -dump:format=b,file=path pid
用IBM的Heap Analyser分析,發現dubbo rpc調用的RpcInvocation對象和taglibs的SimpleForEachIterator對象占用了很大部分內存。
正常來說,這兩種類型的對象都應該可以很快被回收掉,怎麼會占用了那麼大的內存空間?是不是有別的對象引用了它們,導致不能釋放?
再仔細分析,發現RpcInvocation對象都是root refer的,也就是根對象,正常來說根對象應該可以很快就被回收掉的,為什麼在內存中會有那麼多對象?
再查看應用的JVM參數:
-Xms2g -Xmx2g -Xmn256m -XX:SurvivorRatio=8 -XX:ParallelGCThreads=8 -XX:PermSize=512m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled首先發現應用的新生代,即-Xmn256m 設置得太小了。對照上麵RpcInvocation對象占用了226M,SimpleForEachIterator占用了267M內存。
顯然在新生代裏,沒辦法放下那麼多的對象,這些對象必然是被放到老生代(old space)裏去了。
既然RpcInvocation對象和SimpleForEachIterator對象應該都是可以很快被回收了,那麼思路變成,觸發一下線上的FullGC,看下對象有沒有被回收。
在觸發之前,先用jmap -histo pid統計下對象的數量:
34: 136762 4376384 com.alibaba.dubbo.rpc.RpcInvocation
129: 16345 392280 org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
用 jmap -histo:live <pid> 觸發Full GC之後:
294: 625 20000 com.alibaba.dubbo.rpc.RpcInvocation
495: 292 7008 org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
果然數量大大的減少了。
所以結論比較明顯了,新生代(Young generation)的空間太小,導致有一些本應該可以很快就被回收的對象被放到了老生代(Old generation)裏,導致老生代上漲很快,頻繁Full GC。
於是想辦法增加新生代的大小,把JVM參數改為:
-Xms2g -Xmx2g -XX:ParallelGCThreads=8 -XX:PermSize=256m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled因為觀察到PermSize實際上隻用了不到200M,沒有必要設置為512M,浪費內存,所以改為 -XX:PermSize=256m -XX:MaxPermSize=512m 。
另外,把新生代最大限製-Xmn256m 去掉。因為默認的NewRatio = 2,即除了PermSize,新生代大約占內存的1/3,即約(2048 - 256) /3 = 597M。和原來相比增大了一倍不止。
修改上線之後,觀察發現Old Space增長緩慢,FullGC次數大大減少,時間在50ms下,Yong GC都在10ms下,達到了想要的效果。
簡單的GC過程分析
首先來看一張GC的模型圖,很形象:
簡單來說,對於GC,我們了解到這些信息就足夠了。
大部分新對象在Eden Space上分配,當Eden Space滿了,則要用到Survivor Space來回收。YGC的算法是很快的。
多次YGC之後,還存活的對象就會被移到Old Generation(old space)上,當Old Generation滿了的時候,就會FGC,FGC有通常比較慢。
Permanent Space隻要你在開始時分配了足夠大的空間,那它可以不用管。
我們可以得出一些結論:
- 合理減少對象進入老生代;
- Old Space可能會一直增長,有時沒有辦法避免不讓對象進入Old Space,當然也有一些程序是從來都不執行FGC的;
- 是不是盡全力防止對象進入老生代?顯然不是,有些對象如果長久存在在新生代裏,顯然加重了YGC的負擔,多次YGC之後仍然存活的對象顯然應該放到Old Space裏。
理想的GC/內存使用情況
總結下來,可以發現,理想的GC情況應該是這樣的:
Old Space增長緩慢,FullGC次數少,FullGC的時間短(大部情況應該要在1秒內)。
總結:
盡量少加上一些默認參數。這點我很讚同RednaxelaFX的看法,配置了默認參數除了讓後麵調優的人蛋疼之外,沒有太多的幫助。
GC調優就是一個取舍權衡的過程,有得必有失,最好可以在多個不同的實例裏,配置不同的參數,然後進行比較。
有很多命令行工具或者圖形工具可以使用,好的工具事半功倍。
參考:
https://www.alphaworks.ibm.com/tech/heapanalyzer IBM Heap Analyser
https://hllvm.group.iteye.com/group/topic/27945 JVM調優的"標準參數"的各種陷阱,RednaxelaFX 出品,強列推薦
https://www.taobaotesting.com/blogs/2392 JAVA性能剖析1——JVM內存管理與垃圾回收
https://www.oschina.net/translate/using-headless-mode-in-java-se 在 Java SE 平台上使用 Headless 模式
最後更新:2017-04-03 12:56:30