原文连接:http://www.cubrid.org/blog/dev-platform/how-to-tune-java-garbage-collection/java
本篇是GC专家系列的第三篇。在第一篇理解Java垃圾回收中咱们学习了几种不一样的GC算法的处理过程,GC的工做方式,新生代与老年代的区别。因此,你应该已经了解了JDK 7中的5种GC类型,以及每种GC对性能的影响。算法
在第二篇Java垃圾回收的监控中介绍了在真实场景中JVM是如何运行GC,如何监控GC数据以及有哪些工具可用来方便进行GC监控。segmentfault
在本篇中,我将基于真实的案例来介绍一些GC调优的最佳选项。写本篇文章时,我假设你已经理解了前两篇的内容。为了深刻理解本部份内容,你最好先浏览一下前两篇的内容——若是你还没有了解的话。服务器
更精确的说,基于Java的服务是否必定须要GC调优?应该说,GC调优并不是全部Java服务都必须作的事情。固然这是基于你已经使用了下面的选项或事实:并发
经过-Xms
和-Xmx
选项指定了内存大小oracle
使用了-server
选项app
系统未产生太多超时日志jvm
也就是说,若是你未设置内存大小而且你的系统产生了过多的超时日志,恭喜你须要为你的系统执行GC调优。ide
可是,请记住:GC调优是不得已时的选择。工具
思考一下GC调优的深层缘由。垃圾回收器会去清理Java中建立的对象。GC须要清理的对象数据以及GC执行的次数取决于应用建立对象的多少。所以,为了控制GC的执行,首先你须要减小对象的建立。
俗话说“积重难返”。因此咱们须要从小处着手,不然它们将不断壮大直到难以管理。
应该多使用StringBuilder
和StringBuffer
对象替代String
。
减小没必要要的日志输出。
即使如此,面对有些场景咱们依然无能为力。咱们知道解析XML和JSON会占用大量的内存空间。即使咱们尽量少的使用String
,尽量好的优化日志输出,然而在解析XML和JSON时仍然会有大量的内存开销,甚至有10~100MB之多,可咱们很难杜绝XML和JSON的使用。可是请记住:XML和JSON会带来很大的内存开销。
若是应用的内存占用不断提高,你就要开始对其进行GC调优了。我把GC调优的目标分为如下两类:
下降移动到老年代的对象数量
缩短Full GC的执行时间
在Oracle JVM中除了JDK 7及最高版本中引入的G1 GC外,其余的GC都是基于分代回收的。也就是对象会在Eden区中建立,而后不断在Survivor中来回移动。以后若是该对象依然存活,就会被移到老年代中。有些对象,由于占用空间太大以至于在Eden区中建立后就直接移动到了老年代。老年代的GC较新生代会耗时更长,所以减小移动到老年代的对象数量能够下降full GC的频率。减小对象转移到老年代可能会被误解为把对象保留在新生代,然而这是不可能的,相反你能够调整新生代的空间大小。
Full GC的单次执行与Minor GC相比,耗时有较明显的增长。若是执行Full GC占用太长时间(例如超过1秒),在对外服务的链接中就可能会出现超时。
若是企图经过缩小老年代空间的方式来下降Full GC执行时间,可能会面临OutOfMemoryError
或者带来更频繁的Full GC。
若是经过增长老年代空间来减小Full GC执行次数,单次Full GC耗时将会增长。
所以,须要为老年代空间设置适当的大小。
在理解Java垃圾回收的结尾,我说过不要有这样的想法:别人经过某个GC选项得到了明显的性能提高,为何我不直接用这个选项呢。由于不一样的服务所拥有的对象数量和对象的生命周期是不一样的。
一个简单场景,若是执行一个任务须要五个条件:A, B, C, D和E,另一个任务只须要两个条件A和B,哪一个任务会快一些?一般只须要条件A和B的任务会快一些。
Java GC选项的设置也是同样的道理。设置不少选项未必能提升GC执行速度,相反还可能会更加耗时。GC调优的基本规则是对两台或更多的服务器设置不一样的选项,并对比性能表现,而后把被证实能提高性能的选项添加到应用服务器上。请记住这一点。
下表列出了与内存相关的且会影响性能的GC选项:
表1: GC调优须要关注的选项
分类 | 选项 | 说明 |
---|---|---|
堆空间 | -Xms | 启动JVM时的初始堆空间大小 |
-Xmx | 堆空间最大值 | |
新生代空间 | -XX:NewRatio | 新生代与老年代的比例 |
-XX:NewSize | 新生代大小 | |
-XX:SurvivorRatio | Eden区与Survivor区的比例 |
我常常会使用的选项是:-Xms
, -Xmx
和 -XX:NewRatio
,其中-Xms
和-Xmx
是必须的。而如何设置-XX:NewRatio
对性能会有显著的影响。
可能有人会问如何设置永久代(Perm)的大小, 可使用-XX:PermSize
和-XX:MaxPermSize
进行设置,但记住只有发生由Perm空间不足致使的OutOfMemoryError
时才须要设置。
另一个会影响GC性能的选项是GC类型,下表列出了JDK 6.0中能使用的相关设置选项:
表2: GC类型选项
分类 | 选项 | 说明 | |
---|---|---|---|
Serial GC | -XX:+UseSerialGC | ||
Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=<value> |
||
Parallel Compacting GC | -XX:+UseParallelOldGC | ||
CMS GC | -XX:+UseConcMarkSweepGC -XX:UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=<value> -XX:+UseCMSInitiatingOccupancyOnly |
||
G1 | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC |
在JDK6中使用G1时,这两个选项必须同时设置 |
除了G1,其余GC类型都是经过每一个选行列的第一行选项进行设置。一般最不会使用的是Serial GC,它是为client应用优化和设计的。
还有不少其余影响GC性能的选项,但不如上面这些对性能的影响明显。另外设置更多选项未必能优化GC的执行时间。
GC调优过程与通常的性能改进流程很类似,下面会介绍我在GC调优过程当中的流程。
首先须要监控GC状态信息以明确在GC操做过程当中对系统的影响。具体方式能够回顾上一篇文章:Java 垃圾回收的监控。
而后经过GC操做状态,对监控结果进行分析,并判断是否有必要进行GC调优。若是分析结果显示GC耗时在0.1-0.3秒之内的话,通常不须要花费额外的时间作GC调优。然而,若是GC耗时达到1-3秒甚至10秒以上,就须要当即对系统进行GC调优。
可是若是你的应用分配了10GB的内存,且不能下降内存容量的话,实际上是没办法进行GC调优的。这种状况下,你首先要去思考为何须要分配这么大的内存。若是只给应用分配了1GB或者2GB内存,当有OutOfMemeoryError
发生时,你须要经过堆dump来分析验证内存溢出的缘由并进行修复。
注释:
堆dump是把内存状况按必定格式输出到文件,可用于检查Java 内存中的对象和数据状况。可以使用JDK中内置的jmap命令建立堆dump文件。建立文件过程当中,Java进程会中断,所以不要在正常运行时系统上作此操做。
若是决定作GC调优,就须要考虑如何选择GC类型、如何设置内存大小。若是你有多台服务器,可经过为每台服务器设置不一样的GC选项并对比不一样的表现,这一步很重要。
设置GC选项后,至少要收集24小时的GC表现数据,而后就能够着手分析这些数据了。若是足够幸运,经过分析就恰好找到了最合适的GC选项。不然就须要分析GC日志,并分析内存的分配状况。而后经过不一样的调整GC类型和内存大小来找到系统的最优选项。
若是GC结果使人满意,就能够把相应的选项应用到全部服务器并中止GC调优。
下面的章节会详细介绍每一个步骤中的详细过程。
监控Web应用(WAS: Web Application Server)GC运行状态的最好方式是使用jstat命令。在Java 垃圾回收的监控部分已经介绍了如何使用jstat命令,因此这里就直接介绍怎么样来校验结果数据。
下面的例子中列出了JVM未作GC调优时的数据:
$ jstat -gcutil 21719 1s S0 S1 E O P YGC YGCT FGC FGCT GCT 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
看一下表中的YGC和YGCT,YGCT 除以 YGC算出平均单次YGC耗时为0.05秒。也就是说在新生代执行一次垃圾回收的平均耗时为50毫秒。经过这份结果,咱们能够无须关注新生代的垃圾回收。
而后再看一下FGCT和FGC,FGCT除以FGC算出平均单次FGC耗时为19.68秒。也就是平均须要消耗19.68秒来执行一次Full GC。上面的结果(共3次Full GC)多是每次Full GC都耗时19.68秒,也有多是其中两次都只耗时1秒,而另一次却消耗了58秒。然而无论哪一种状况,都迫切须要进行GC调优。
固然也能够经过jstat来校验结果,不过度析GC的最好方式是使用-verbosegc
选项来启动JVM。在前面的文章中我已经详细介绍了生成日志的方式以及如何进行分析。就分析-verbosegc日志而言,HPJMeter是我最偏心的工具,由于它简单易用。使用HPJMeter能够轻松获取GC执行时间的开销以及GC发生的频率。
若是GC执行时间知足如下判断条件,那么GC调优并没那么必须。
Minor GC执行迅速(50毫秒之内)
Minor GC执行不频繁(间隔10秒左右一次)
Full GC执行迅速(1秒之内)
Full GC执行不频繁(间隔10分钟左右一次)
括号内的值并不是绝对,依据应用的服务状态会有不一样。有些服务可能要求Full GC处理速度不能超过0.9秒,另一些服务可能会宽松些。所以校验GC结果并根据具体的服务须要,决定是否要进行GC调优。
在校验GC状态时,不要只关心Minor GC和Full GC的耗时,也要GC执行次数也一样重要。若是新生代过小,Minor GC就会频繁执行(甚至每间隔1秒就要执行一次)。另外,新生代过小致使转移到老年代的对象增多,也会引发Full GC的频繁执行。所以使用`-gccapacity`配合jstat命令,以检查内存空间的使用状况。
Oracle JVM提供了5种GC类型,若是是低于JDK 7的版本,可使用Parallel GC, Parallel Compacting GC, CMS GC。固然,到底选哪个并无统一的准则或标准。
因此如何选择合适的GC类型?推荐方案是将这三种GC都应用到应用中进行对比。不过能够明确的是CMS GC确定比Parallel GCs更快,即然这样只使用CMS GC便好。然而CMS GC也有出问题的时候,一般Full GC中使用CMS GC会执行更快,若是CMS GC的并发模式失败,则会出现比Parallel GCs慢的状况。
咱们来深刻看一下并发模式失败的场景。
Parallel GC与CMS GC最大的区别在于压缩任务。压缩任务经过压缩内存使用来移除内存中的碎片空间,以清理两块已分配使用的内存空间中的间隙。
在Parallel GC中,只要执行Full GC便会进行内存压缩,所以耗时更长。不过Full GC以后,由于压缩的原故,能够分配连续的空间,因此内存的分配速度为更快一些。
与之相反,CMS GC的执行中并不会伴随内存压缩,所以GC速度会更快一些。然而,由于未作内存压缩, GC清理过程当中释放的内存便会成为空闲空间。由于空间不连续,可能会致使在建立大对象时空间不足。例如,若是老年代尚有300M空闲,却不能为10MB的对象分配足够的连续空间。这时便会发生并发模式失败的警告,并触发内存压缩。若是使用CMS GC,在内存压缩过程当中可能会比Parallel GCs更为耗时,也可能会带来其余问题。关于"并发模式失败"更详细的介绍能够看Oracle 工程师的文章:理解CMS GC 日志。
结论就是,要为你的系统寻找合适的GC类型。
每一个系统都有一个最适当的GC类型,因此你须要找到这个GC类型。若是你有6台服务器,建议你为每两组设置相同的选项,并经过-verbosegc
选项对结果进行分析和比较。
下面先列出内存大小与GC执行次数、每次GC耗时之间的关系:
大内存
会下降GC执行次数
相应的会增长GC执行耗时
小内存
会缩知单次GC耗时
相应的会增长GC执行次数
固然,关于使用大内存仍是小内存并无惟一正确的答案。若是服务器资源足够且Full GC执行耗时能控制在1秒之内,使用10GB的内存也是能够的。但大多数时候若是设置内存为10GB,GC执行效果并不尽人意,执行一次Full GC可能要消耗10~30秒(具体时长也会根据对象大小状况而不一样)。
既然如此,如何正确设置内存大小。一般状况下,我会推荐500MB大小。这不是说你要把本身的WAS(Web Application Server)内存选项设置为-Xms500
和-Xmx500m
。基于当前未调优时的场景,检查Full GC以后内存大小变化。若是Full GC以后尚有300MB空间剩余,这样最好把内存设置到1GB(300MB(默认使用) + 500MB(老年代最小容量) + 200MB(空闲空间))。这意味着你应该才老年代至少设置500MB空间。若是你有3台服务器,能够分别设置1GB、1.5GB和2GB,并检查每台机器的执行结果。
理论上,根据内存大小不一样单次执行GC速度应该是1GB > 1.5GB > 2GB,因此1GB的内存会中三个之中GC速度最快的。但并不能保证1GB的内存Full GC耗时1秒,2GB的内存Full GC耗时2秒。实际耗时与机器性能和对象大小也有关系。因此最好的度量方式是设置每种可能性并分析他们的监控结果。
有设置内存大小时,还须要设置另一选项:NewRatio
。NewRatio
是新生代与老年代的比值的倒数(即老年代与新生代的比值)。若是XX:NewRatio=1
,就是说新生代 : 老年代的比值为1:1。对于1GB内存,就是新生代与老年代各500MB。若是NewRatio
的值是2,则是新生代 : 老年代的值为1:2。所以比值设置的越大,老年代的空间就越大,相应的新生代空间会越小。
设置NewRatio
也不是一件重要的事,但可能会对整个GC性能带来严重影响。若是新生代过小,对象就会转移到老年代,引发频繁的Full GC,致使更多的耗时。
你可能简单的认为设置NewRatio=1
会带来最佳的效果,然而并不是如此。把NewRatio
设置为2或3更容易带来好的GC表现。固然我也实际遇到过一些这样的例子。
完成GC调优的最快途径是什么?经过对比性能测试的结果是获得GC调优结果的最快途径。经过为每一个服务器设置不一样的选项并观察GC状态,最好能观察1到2天的数据。若是是经过性能测试来作GC调优的话,要为每一个服务器准备相同的负载和业务操做。请求比例的分配也要与业务条件相一致。然而即使是专业的性能测试人员,准备精确的负载数据也并不是易事,一般须要花费很大精力来作准备。因此更简捷的GC调优方式就是对业务应用准备GC选项,而后经过等待GC结果并进行分析,尽管可能须要更长的等待时间。
在应用GC选项并设置-verbosegc
后,能够经过tail
命令检查日志是否定期望的方式正常输出。若是选项未精确的设置或者没有定期望输出,你所花费的时间都将白费。若是日志输出与指望相符,等待1到2天的运行后即可检查和分析结果。最简单的方式是把日志文件复制到本地PC,并使用HPJMeter进行分析。
分析过程当中主要关注如下数据,下面列表是按我本身定义的优先级列出的。其中决定GC选项的最重要的数据是Full GC执行时间。
Full GC(平均)耗时
Minor GC(平均)耗时
Full GC执行间隔
MinorGC执行间隔
Full GC总体耗时
Minor GC总体耗时
GC总体耗时
Full GC执行次数
Minor GC执行次数
若是足够幸运,你能刚好找到合适的GC选项,一般你并没这么幸运。执行GC调优时必定要格外当心,由于若是你试图一次就完成GC调优,获得的可能会是OutOfMemoryError
。
上面咱们对于GC调优的讨论还仅是纸上谈兵,如今开始咱们看一些具体的GC调优的案例。
这个例子是为服务S进行的GC优化。对于这个新上线的服务S,在执行Full GC时有些过于耗时。
先看一下jstat -gcutil
的结果:
S0 S1 E O P YGC YGCT FGC FGCT GCT 12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
在开始进行调优时不用太关心持久代空间的设置,相对而言YGC的数值更值得关注。
从上面的结果中咱们可算出执行Minor GC和Full GC的平均时间上的开销,以下表:
表3:服务S执行Minor GC和Full GC的平均耗时
GC类型 | GC 执行次数 | GC执行时间 | 平均耗时 |
---|---|---|---|
Minor GC | 54 | 2.047 | 37 ms |
Full GC | 5 | 6.946 | 1389 ms |
对于Minor GC来讲,37 ms还不算坏,而Full GC的平均耗时1.389 s对于系统来讲在执行Full GC时可能会致使频繁的超时现象,例如DB超时设置为1 s的话就会发生超时。因此这个案例中的系统须要进行GC调优。
首先在开始GC调优以前先检查当前的内存设置。可使用jstat -gccapacity
选项查看内存的使用状况。下面是服务S的检查结果:
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC 212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5
其中关键的数据以下:
新生代使用:212, 992 KB(约208 MB)
老年代使用:1,884,160 KB(约1.8 GB)
因此除去持久代以外的内存分配为2 GB,且新生代 : 老年代为 1:9 (即NewRatio=9
)。为了看到更详细的信息,对系统的三个不一样实现均设置了-verbosegc
并分别设置了NewRatio
选项,除此以外未添加其余选项。
NewRatio = 2
NewRatio = 3
NewRatio = 4
一天以后检查GC时日志时幸运的发生,在设置NewRatio
以后还没有有Full GC发生。
发生了什么?由于大多数对象在建立以后不久就被销毁,因此新生代里的对象在移到老年代以前就被销毁掉了。
既然如此,就不必再设置其余选项,只是选择好最佳的NewRatio
便可。如何选取最佳NewRatio?只能逐个分析设置不一样NewRatio
值时的Minor GC的平均耗时。
上面三个NewRatio
设置对应的Minor GC平均耗时以下:
NewRatio=2: 45ms
NewRatio=3: 34ms
NewRatio=4: 30ms
由于NewRatio=4
时Minor GC具备最小的耗时,因此就是咱们选择的最佳设置,即使此时新生代的空间相对较小。应用此选项后,服务再也没有Full GC发生。
下面是系统从新设置过选项后,某天经过jstat -gcutil
查看到的结果:
S0 S1 E O P YGC YGCT FGC FGCT GCT 8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219
你可能认为由于系统接收的请求太少以至于GC发生频率较低,然而在Minor GC执行了2,424次的状况下系统未发生Full GC。
下面介绍的是服务A的例子。咱们在公司的应用性能管理平台(APM: Application Performance Manager)上发现服务A的JVM周期性的出现长时间的停顿(超过8秒未有响应)的现象。因此咱们决定对其进行GC调优。通过排查咱们发现此系统在执行Full GC时太过耗时,须要进行优化。
在着手优化以前,咱们为系统加上了-verbosegc
选项,输出结果以下图:
图1:GC调优以前的GC耗时
上图是HPJMeter自动分析结果后提供的系统GC随着JVM运行的耗时图。X-轴是JVM从启动后的运行时间轴,Y-轴是每次GC的响应时间。其中绿色的是Full GC使用的CMS垃圾回收的耗时,蓝色的是Minor GC使用的Parallel Scavenge垃圾回收的耗时。
前面我说过CMS GC是最快的,但上图可看到有场景耗时竟达到15秒之多。什么缘由致使这种后果?回想一下我前面说过的:当内存压缩时CMS将会变慢。另外服务A设置了-Xms1g
和-Xmx4g
的选项,操做系统为其分配的内存为4 GB。
而后我把GC类型由GMS换成了Parallel GC,并把内存大小设置为2G,NewRatio
设置为3。一段时间以后经过jstat -gcutil
查看到的结果以下:
S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890
Full GC的速度提高了,与4GB内存时的15秒相比,如今平均每次只须要3秒。但3秒仍然不尽人意,因此我设计了如下六组选项:
-XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2
-XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3
-XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3
-XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2
-XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3
-XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3
哪个会更快呢?结果显示内存越小,速度越快。下图是第六组选项的GC持续时长分布图,表明了最优的GC性能提高。图中看到最慢的为1.7秒,而平均值下降到1秒之内。
图2:使用第六组选项后的GC耗时
所以我把服务A的GC选项调整为了第六组中的设置,然而天天夜里却连续发生了OutOfMemoryError
。个中艰辛再也不细说,简而言之就是批量的数据处理任务致使了JVM内存泄露。到此为止,全部的问题都明了了。
若是只对GC日志作短期的观察例把GC调优的结果应用到全部服务器上是一件很是危险的事情。必定要记住,若是GC调优可以顺利执行而无端障只有一条途径:像分析GC日志同样分析系统的每个服务操做。
上面经过两个GC调优的案例演示了GC调优的具体处理过程。如我所述,案例中的GC选项能够不作调整的应用到那些具备相同CPU、操做系统和 JDK 版本以及执行相同功能的服务上去。然而不要把这些选项应用到你的系统上,由于他们未必适用。
我执行GC调优通常基于经验而无需经过堆dump后对内存进行详细的分析,尽管精确的内存状态可能会带来更好的GC调优结果。在通常情景,若是内存负载较低时,经过分析内存对象可能效果更好,不过若是服务负载较高,内存空间使用较多时,更推荐基于经验来作GC调优。
我曾经在一些服务上对G1 GC作过性能测试,不过尚未全面使用。结果证实G1 GC执行速度比其余任何GC都要快,不过须要把JDK升级到 JDK 7 才能享受到G1带来的性能提高,另外G1的稳定性目前尚不能彻底保证,没有人知道是否会带来严重的bug。因此大范围使用 G1 还尚待时日。
当 JDK 7 稳定之后(并非说它当前不稳定),而且WAS针对JDK 7作过优化以后,G1也许会稳定的运行在服务器上,到那时也许就再也不须要进行GC调优了。
更多GC调优的细节能够在Slideshare上搜索相关材料。我最推荐的是Twitter 工程师 Attila Szegedi写的这篇我在Twitter学到的关于JVM调优的一切,有时间能够学习一下。
做者:Sangmin Lee, 性能实验室高级工程师,NHN公司