先转载一般来自豆瓣的文章:
JVM参数调优是一个很头痛的问题,可能和应用有关系,下面是本人一些调优的实践经验,希望对读者能有帮助,环境LinuxAS4,resin2.1.17,JDK6.0,2CPU,4G内存,dell2950服务器,网站是shedewang.com,新手可能觉得这文章没有用。
一:串行垃圾回收,也就是默认配置,完成10万request用时153秒,JVM参数配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ";
这种配置一般在resin启动24小时内似乎没有大问题,网站可以正常访问,但查看日志发现,在接近24小时时,Full GC执行越来越频繁,大约每隔3分钟就有一次Full GC,每次Full GC系统会停顿6秒左右,作为一个网站来说,用户等待6秒恐怕太长了,所以这种方式有待改善。MaxTenuringThreshold=7表示一个对象如果在救助空间移动7次还没有被回收就放入年老代,GCTimeRatio=19表示java可以用5%的时间来做垃圾回收,1/(1+19)=1 /20=5%。
二:并行回收,完成10万request用时117秒,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xmx2048M -Xms2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=500 -XX:+UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 ";
并行回收我尝试过多种组合配置,似乎都没什么用,resin启动3小时左右就会停顿,时间超过10 秒。也有可能是参数设置不够好的原因,MaxGCPauseMillis表示GC最大停顿时间,在resin刚启动还没有执行Full GC时系统是正常的,但一旦执行Full GC,MaxGCPauseMillis根本没有用,停顿时间可能超过20秒,之后会发生什么我也不再关心了,赶紧重启resin,尝试其他回收策略。
三:并发回收,完成10万request用时60秒,比并行回收差不多快一倍,是默认回收策略性能的2.5倍,配置如下:
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 ";
这个配置虽然不会出现10秒连不上的情况,但系统重启3个小时左右,每隔几分钟就会有5秒连不上的情况,查看gc.log,发现在执行ParNewGC时有个promotion failed错误,从而转向执行Full GC,造成系统停顿,而且会很频繁,每隔几分钟就有一次,所以还得改善。UseCMSCompactAtFullCollection是表是执行Full GC后对内存进行整理压缩,免得产生内存碎片,CMSFullGCsBeforeCompaction=N表示执行N次Full GC后执行内存压缩。
四:增量回收,完成10万request用时171秒,太慢了,配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xincgc ";
似乎回收得也不太干净,而且也对性能有较大影响,不值得试。
五:并发回收的I-CMS模式,和增量回收差不多,完成10万request用时170秒。
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=10 -XX:-TraceClassUnloading ";
采用了sun推荐的参数,回收效果不好,照样有停顿,数小时之内就会频繁出现停顿,什么sun推荐的参数,照样不好使。
六:递增式低暂停收集器,还叫什么火车式回收,不知道属于哪个系,完成10万request用时153秒
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -Xloggc:log/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseTrainGC ";
该配置效果也不好,影响性能,所以没试。
七:相比之下,还是并发回收比较好,性能比较高,只要能解决ParNewGC(并行回收年轻代)时的promotion failed错误就一切好办了,查了很多文章,发现引起promotion failed错误的原因是CMS来不及回收(CMS默认在年老代占到90%左右才会执行),年老代又没有足够的空间供GC把一些活的对象从年轻代移到年老代,所以执行Full GC。CMSInitiatingOccupancyFraction=70表示年老代占到约70%时就开始执行CMS,这样就不会出现Full GC了。SoftRefLRUPolicyMSPerMB这个参数也是我认为比较有用的,官方解释是softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap,我觉得没必要等1秒,所以设置成0。配置如下
$JAVA_ARGS .= " -Dresin.home=$SERVER_ROOT -server -Xms2048M -Xmx2048M -Xmn512M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=7 -XX:GCTimeRatio=19 -Xnoclassgc -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSPermGenSweepingEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=70 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -Xloggc:log/gc.log ";
上面这个配置内存上升的很慢,24小时之内几乎没有停顿现象,最长的只停滞了0.8s,ParNew GC每30秒左右才执行一次,每次回收约0.2秒,看来问题应该暂时解决了。
参数不明白的可以上网查,本人认为比较重要的几个参数是:-Xms -Xmx -Xmn MaxTenuringThreshold GCTimeRatio UseConcMarkSweepGC CMSInitiatingOccupancyFraction SoftRefLRUPolicyMSPerMB
CMS缺点
- CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
- 需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!
- CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。
总得来说,CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。
啥时候用CMS
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。
二、个人使用总结:
1、在机器内存不大的情况下(小于应用所需内存的2倍以上),不建议使用CMS。因为CMS的执行需要开销比较大的内存,如果内存不足可能引起JVM崩溃(典型例子是在目录下面生成hs_pidxxx日志)
2、在内存足够大的时候,推荐使用CMS,因为执行效率最高。
3、开启-XX:-HeapDumpOnOutOfMemoryError,在OMM的时候会生产java_pid.hrof文件,通过MAT(http://www.eclipse.org/mat/downloads.php)工具可以解析该文件,然后分析出内存异常的可疑地方。
三、内存分析工具:
1、jmap -histo pid —— 列出当前应用的实例数目,可分析哪些实例比较多,占用内存比较大
jmap heap pid —— 打印jvm heap的情况
2、jstat —— 分析GC的工具。
1) jstat -gcutil pid ——
jstat -gcutil pid 1000 20
2)jstat -gccapacity pid
异常情况的例子
jstat -gcutil pid 1000 20
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 99.99 82.51 53.11 2409 1.205 10117 7250.393 7251.598
0.00 0.00 83.42 82.55 53.10 2409 1.205 10118 7252.650 7253.855
0.00 0.00 56.06 82.46 53.10 2410 1.205 10120 7254.467 7255.672
0.00 0.00 32.11 82.55 53.10 2411 1.205 10121 7256.673 7257.877
0.00 0.00 99.99 82.55 53.10 2412 1.205 10123 7257.026 7258.231
0.00 0.00 76.00 82.50 53.10 2412 1.205 10124 7259.241 7260.446
这个数据显示Full GC频繁发生(FGCT每一秒都有操作)。
如果GC太频繁,在CMS模式下的话,可以设置CMSInitiatingOccupancyFraction=90,这样在老生代占到90%左右才会执行GC。但这样也可能引起promotion failed问题(前面有介绍)。另一种方法是,加大-Xmm配置。
正常情况的例子
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031
参数含义:
S0:Heap上的 Survivor space 0 段已使用空间的百分比
S1:Heap上的 Survivor space 1 段已使用空间的百分比
E: Heap上的 Eden space 段已使用空间的百分比
O: Heap上的 Old space 段已使用空间的百分比
P: Perm space 已使用空间的百分比
YGC:从程序启动到采样时发生Young GC的次数
YGCT:Young GC所用的时间(单位秒)
FGC:从程序启动到采样时发生Full GC的次数
FGCT:Full GC所用的时间(单位秒)
GCT:用于垃圾回收的总时间(单位秒)
3、查看堆内存情况:java -verbosegc classFile
内存调优过程:
1、开启-XX:-HeapDumpOnOutOfMemoryError,在OMM的时候会生产java_pid.hrof文件,通过MAT(http://www.eclipse.org/mat/downloads.php)工具可以解析该文件,然后分析出内存异常的可疑地方.开启gc日志:在启动参数里添加-Xloggc:log.log(缺省的GC日志时输出到终端的,使用-Xloggc:log.log也可以输出到指定的文件。需要注意这个参数隐式的设置了参数-XX:+PrintGC和-XX:+PrintGCTimeStamps,但为了以防在新版本的JVM中有任何变化,我仍建议显示的设置这些参数。
)。或者动态开启:通过JDK自带的jinfo工具设置这些参数,或者是通过JMX客户端调用HotSpotDiagnostic MXBean的
setVMOption方法来设置这些参数。
3、用jstat -guutill pid 查看gc情况
4、如果full GC频繁,查看GC日志,观察每次FGC的频率以及各个区的回收情况
5、然后再配合使用jstat与jmap查看是否有泄露问题,分析内存泄露最佳组合就是jamp dump + MAT,先dump内存,然后MAT分析来定位问题,要jmap -heap需要慎用,在用cms gc的情况下,有些时候jmap -heap会循环输出,然后就卡死了
6、最后使用jstack排查各线程的状态,包括用户线程和虚拟机线程,同时结合Lock信息来检测是否发生了死锁和死锁的线程.
7、另外在用top -H看到占用CPU非常高的pid时,可以转换成16进制后在jstack dump出来的文件中搜索,看看到底是什么线程占用了CPU
总之,GC问题是一个需要不断跟进的问题,它会随着业务与技术发展而不断出现,需要用到很多方法与经验.