成为java gc专家系列(3) — 如何优化java垃圾回收机制
本文是成为java gc专家系列文章的第三篇。在第一篇《成为javagc专家part i — 深入浅出java垃圾回收机制》中我们学习了不同gc算法的执行过程,gc是如何工作的,什么是新生代和老年代,你应该了解的jdk7中的5种gc类型,以及这5种类型对于应用性能的影响。
在第二篇《成为javagc专家part ii — 如何监控java垃圾回收机制》,我解释了jvm实际上是如何执行垃圾回收的,我们如何监控gc,以及那哪些具可以让我们的工作更快,更高效。在第三篇文章中,我们会基于实际的例子来解释一些优化gc的最佳实践。我认为在阅读本篇文章之前,你已经很好地理解了之前的文章,因此,为了你能够更好地学习本文,如果你还没有读过之前的两篇文章话,请先阅读。
为什么需要优化gc
或者说的更确切一些,对于基于java的服务,是否有必要优化gc?应该说,对于所有的基于java的服务,并不总是需要进行gc优化,但前提是所运行的基于java的系统,包含了如下参数或行为:
- 已经通过 -xms 和–xmx 设置了内存大小
- 包含了 -server 参数
- 系统中没有超时日志等错误日志
换句话说,如果你没有设定内存的大小,并且系统充斥着大量的超时日志时,你就需要在你的系统中进行gc优化了。
但是,你需要时刻铭记一条:gc优化永远是最后一项任务。
想一下进行gc优化的最根本原因,垃圾收集器清除在java程序中创建的对象,gc执行的次数即需要被垃圾收集器清理的对象个数,与创建对象的数量成正比,因此,首先你应该减少创建对象的数量。
俗话说的好,“冰冻三尺非一日之寒”。我们应该从小事做起,否则日积月累就会很难管理。
- 我们需要使用stringbuilder 或者stringbuffer 来替代string
- 应该尽量少的输出日志
但是,我们知道有些情况会让我们束手无策,我们眼睁睁的看着xml以及json解析占用了大量的内存。即便我们已经尽可能少的使用string以及尽量少的输出日志,大量的临时内存被用于xml或者json解析,例如10-100mb。但是,舍弃xml和json是很难的。我们只要知道,他会占用很多内存。
如果应用内存使用量经过几次重复调整之后有所改善,你就可以开始gc优化了。
我为gc优化归纳了两个目的:
- 一个是将转移到老年代的对象数量降到最少
- 另一个是减少full gc的执行时间
将转移到老年代的对象数量降到最少
按代的gc机制由oracle jvm提供,不包括可以在jdk7以及更高版本中使用的g1 gc。换句话说,对象被创建在伊甸园空间,而后转化到幸存者空间,最终剩余的对象被送到老年代。某些比较大的对象会在被创建在伊甸园空间后,直接转移到老年代空间。老年代空间上的gc处理会新生代花费更多的时间。因此,减少被移到老年代对象的数据可以显著地减少full gc的频率。减少被移到老年代空间的对象的数量,可能被误解为将对象留在新生代。但是,这是不可能的。取而代之,你可以调整新生代空间的大小。
减少full gc执行时间
full gc的执行时间比minor gc要长很多。因此,如果full gc花费了太多的时间(超过1秒),一些连接的部分可能会发生超时错误。
- 如果你试图通过消减老年代空间来减少full gc的执行时间,可能会导致outofmemoryerror 或者 full gc执行的次数会增加。
- 与之相反,如果你试图通过增加老年代空间来减少full gc执行次数,执行时间会增加。
因此,你需要将老年代空间设定为一个“合适”的值。
影响gc性能的参数
正如我们在第二篇文章结尾提到的,不要幻想“某个人设定了gc参数后性能得到极大的提高,我们为什么不和他用一样的参数?”,因为不同的web服务所创建对象的大小和他们的生命周期都不尽相同。
简单来说,如果一个任务的执行条件是a,b,c,d和e,同样的任务执行条件换为a和b,你会觉得哪个更快?从一般人的直觉来看,在a和b条件下执行的任务会更快。
java gc参数也是相同的道理,设定一些参数不但没有提高gc执行速度,反而可能导致他更慢。gc优化的最基本原则是将不同的gc参数用于2台或者多台服务器,并进行对比,并将那些被证明提高了性能或者减少了gc执行时间的参数应用于服务器。请谨记这一点。
下面这个表格列出了gc参数中与内存大小相关的,可以影响性能的参数。
表1:gc优化需要考虑的java参数
定义 |
参数 |
描述 |
堆内存空间 |
-xms |
heap area size when starting jvm 启动jvm时的堆内存空间。 |
-xmx |
maximum heap area size 堆内存最大限制 |
|
新生代空间 |
-xx:newratio |
ratio of new area and old area 新生代和老年代的占比 |
-xx:newsize |
new area size 新生代空间 |
|
-xx:survivorratio |
ratio ofedenarea and survivor area 伊甸园空间和幸存者空间的占比 |
我在进行gc优化时经常使用-xms,-xmx和-xx:newratio。-xms和-xmx是必须的。你如何设定newratio 会对gc性能产生十分显著的影响。有些人可能会问如何设定perm区域的大小?你可以通过-xx:permsize 和-xx:maxpermsize参数来设定,
当outofmemoryerror 错误发生并且是由于perm空间不足导致时,另一个可能影响gc性能的参数是gc类型。下表列出了所有可选的gc类型(基于jdk6.0)
表2:gc类型可选参数
分类 |
参数 |
备考 |
serial gc |
-xx:+useserialgc |
|
parallel gc |
-xx:+useparallelgc |
|
parallel compacting gc |
-xx:+useparalleloldgc |
|
cms gc |
-xx:+useconcmarksweepgc |
|
g1 |
-xx:+unlockexperimentalvmoptions |
在jdk6中这两个参数必须同时使用 |
除了g1 gc,可以通过每种类型第一行的参数来切换gc类型。最常用的gc类型是serial gc。他专门针对客户端系统进行了优化。
影响gc性能的参数有很多,但是上面提到的参数会带来最显著的效果。请牢记,设定过多的参数不一定会减少gc执行时间。
gc优化过程
gc优化的过程与大多数性能改善的过程及其类似。下面是我使用的gc优化过程。
1.监控gc状态
首先你需要监控gc来检查在系统执行过程中gc的各种状态。请参考前一篇文章中提到的监控方法 成为javagc专家part ii — 如何监控java垃圾回收机制。
2.在分析监控结果后,决定是否进行gc优化
在检查gc状态的过程中,你应该分析监控结果以便决定是否进行gc优化,如果分析结果表明执行gc的时间只有0.1-0.3秒,那你就没必要浪费时间去进行gc优化。但是,如果gc的执行时间是1-3秒,或者超过10秒,gc将势在必行。
但是,如果你已经为java分配了10gb的内存,并且不能再减少内存大小,你将无法再对gc进行优化。在进行gc优化之前,你必须想清楚你为什么要分配如此大的内存空间。假如当你分1 gb 或 2 gb内存时出现outofmemoryerror ,你应该执行堆内存转储(heap dump),并消除隐患。
注意:
堆内存转储是一个用来检查java内存中的对象和数据的文件。该文件可以通过执行jdk中的jmap命令来创建。在创建文件的过程中,java程序会暂停,因此不要再系统执行过程中创建该文件。
你可以在互联网上搜索堆内存[s1] 转储的详细说明。对于韩国的读者,可以参考我去年发布的书: the story of troubleshooting for java developers and system operators (sangmin lee, hanbit media, 2011, 416 pages)。
3. 调整gc类型/内存空间
如果你已经决定要进行gc优化,那么就要选择gc类型和设定内存空间。在这时,如果你有几台不同服务器,请时刻牢记,检查每一台服务器的gc参数,并进行有针对性的优化。
4.分析结果
在调整了gc参数并持续收集24小时之后,开始对结果进行分析,如果你幸运的话,你就找到那些最适合系统的gc参数。反之,你需要通过分析日志来检查内存是如何被分配的。然后你需要通过不断的调整gc类型和内存空间大小一边找到最佳的参数。
5. 如果结果令人满意,你可以将该参数应用于所有的服务器,并停止gc优化
有过gc优化结果令人满意,你可以应用于所有的服务器,下面的章节中,我们将看到每个步骤的具体任务。
监控gc状态及分析结果
查看运行中的web application server (was)的gc状态的最佳方法是通过jstat命令,在第二篇文章成为javagc专家part ii — 如何监控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得到0.050秒(50毫秒)。这意味着新生代空间上的gc操作平均花费50毫秒。在这种情况,你大可不必担心新生代空间上执行的gc操作。
接下来,我们来看一下fgct 和fgc。,计算fgct/ fgc得到19.68秒,这意味着gc的平均执行时间为19.68秒,可能是每次花费19.68秒执行了三次,也可能是其中的两次执行了1秒而另一次执行了58秒。不论哪种情况,都需要进行gc优化。
通过jstat 命令可以很轻易地查看gc状态,但是,分析gc的最佳方式是通过–verbosegc参数来生成日志,在之前的文章中我已经解释了如何分析这些日志,hpjmeter 是我个人最喜欢的用于分析-verbosegc 日志的工具。他很易于使用和分析结果。通过hpjmeter你可以很轻易查看gc执行时间以及gc发生频率。如果gc执行时间满足下面所有的条件,就意味着无需进行gc优化了。
- minor gc执行的很快(小于50ms)
- minor gc执行的并不频繁(大概10秒一次)
- full gc执行的很快(小于1s)
- full gc执行的并不频繁(10分钟一次)
上面提到的数字并不是绝对的;他们根据服务状态的不同而有所区别,某些服务可能满足于full gc每次0.9秒的速度,但另一些可能不是。因此,针对不同的服务设定不同的值以决定是否进行gc优化。
在查看gc状态的时候有件事你需要特别注意,那就是不要只关注minor gc 和full gc的执行时间。还要关注gc执行的次数,例如,当新生代空间较小时,minor gc会过于频繁的执行(有时每秒超过1次)。另外,转移到老年代的对象数增多,则会导致full gc执行次数增多。因此,别忘了加上–gccapacity参数来查看具体占用了多少空间。
设定gc类型/内存空间大小
- 设定gc类型
oraclejvm有5种gc类型,但是在jdk7之前的版本中,只能在parallel gc, parallel compacting gc 和cms gc之中选择一个,对于选择哪个没有明确的原则和规则。
这样的话,我们该如何选择呢?强烈建议三者都选,但是,有一点是很明确的:cms gc比parallel gcs更快。如果真的如此,那么就选cms gc了。但是,cms gc也不总是更快。整体来看,cms gc模式下的full gc执行更快,不过,一旦出现并行模式失败,他将比parallel gc更慢。
并发模式失败
我们来详细讲解一下并发模式失败。
parallel gc 和 cms gc 最大的不同来自于压缩任务。压缩任务是通过删除已分配内存空间中的空白空间以便压缩内存,清理内存碎片。
在parallel gc模式下,压缩工作在full gc执行时进行,这会费很多时间,但是,在执行完full gc之后,由于能够顺序地分配空间,随后的内存能够被更快的分配。
与之相反的,cms gc并不进行压缩处理,因此,cms gc执行的更快。但是,由于没有压缩,在进行磁盘清理之前,内存中会有很多空白空间。这就是说,可能没有足够的空间存储大的对象,例如,虽然老年代空间还有300mb空间,但是一些10mb的对象无法被顺序的存储。在这种情况下,会出现“并行模式失败”警告,并执行压缩处理。在cms gc模式下,压缩处理的执行时间要比parallel gcs长很多。另外,这还将导致另外一个问题。关于并发模式失败的详细说明,可以参考oracle工程师撰写的understanding cms gc logs。
综上所述,你需要找到最适合你的系统的gc类型。
每个系统都有最适合他的gc类型等着你去寻找,如果你有6台服务器。我建议你每两台设置相同的参数。并添加 –verbosegc参数,分析结果。
- 设定内存空间大小
下表展示了内存空间大小,gc执行次数以及gc执行时间三者间的关系。
- 大内存空间
- 减小gc执行次数
- 增加gc执行时间
- 小内存空间
- 减小gc执行时间
- 增加gc执行次数
关于如何设置内存空间的大小,没有唯一的标准答案。如果服务器资源足够,而且full gc也可能在1秒内完成,设置为10gb当然可行。。但绝大多数服务器并不是这样,当内存设为10gb时,可能要花费10~30秒来执行full gc。当然,执行时间会随对象的大小而改变。
鉴于如此,我们应该如何设定内存空间大小呢?一般来说,我建议为500mb。不过请注意这不是让你将was的内存参数设置为–xms500m 和–xmx500m。根据优化gc之前的状态,如果full gc执行之后内存空间剩余300mb,那么最好将内存设置为1gb(300mb(默认程序占用)+ 500mb(老年代最小空间)+200mb(空闲内存))。也就是说你要为老年代额外设置500mb。因此,如果你有三个执行服务器,内存分别设置为1gb,1.5gb,2gb,并且检查结果。
理论上来讲,gc执行速度应该遵循1gb> 1.5gb> 2gb,因此1gb执行gc速度最快。但是并不说明1gb空间的full gc会花费1秒而2gb空间会花费2秒。时间取决于服务器的性能和对象的大小。因此,最佳的方式是建立尽可能多的衡量指标来监控他们。
对于内存空间大小,你应该额外设定newratio参数。newratio参数是新生代和老年代空间的比例,即xx:newratio=1意味着新生代与老年代之比为1:1。对于1gb来说就是新生代和老年代各500mb。如果newratio为2,意味着新生代老年代之比为1:2,因此该值越大,老年代空间越大,新生代空间越小。
这看似一件不是很重要的事情,但newratio参数会显著地影响整个gc的性能。如果新生代空间很小,会用更多的对象被转移到老年代空间,这样导致频繁的full gc,增加暂停时间。
你可以简单的认为newratio 为1是最佳的选择,但是,有时可能设置为2或3更好,我就见过很多这样的例子。
如何最快的完成gc优化?对比性能测试的结果应该是最快地方法,为每一台服务器设置不同的参数并监控他们的状态,强烈建议至少监控1或2天的数据。但是,当你对gc优化是,你要确保每次执行相同的负载。并且请求的比率,例如url都应该是一致的。不过,即便对于专业测试人员要想精确的控制负载也是很难的,并要花费大量的时间准备。因此,相对来说比较方便和容易的方法是调整才参数,之后花费较长的时间收集结果。
分析gc优化结果
在设置了gc参数以及-verbosegc参数之后,通过tail命令确保日志被正确的生成。如果参数设置的不正确或者日志没有生成,你将白白浪费你的时间。如果日志正确的话,持续收集1到2天。随后最好将日志下载到本地pc并用hpjmeter来分析
- full gc 执行时间
- minor gc执行时间
- full gc 执行间隔
- minor gc 执行间隔
- entire full gc 执行时间
- entire minor gc 执行时间
- entire gc 执行时间
- full gc e执行时间
- minor gc 执行时间
找到最佳的gc参数是件非常幸运的事情,然而在大多数场合,我们并不会得到幸运之神的眷顾,在进行gc优化时要尽量小心谨慎,想一步完成优化往往会导致outofmemoryerror 。
优化示例
好了,我们一直在纸上谈兵,现在我们看一些实际的gc优化的例子。
示例1
下面这个例子针对 service s的优化,对于最近被部署的 service 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
最左边的perm 空间对于最初的gc优化不是很重要,这一次ygc参数的值更加有用。
minor gc和full gc的平均值如下表所示
表3:service s的minor gc 和full gc的平均执行时间
gc 类型 |
gc 执行次数 |
gc 执行时间 |
平均 |
minor gc |
54 |
2.047 |
37 ms |
full gc |
5 |
6.946 |
1,389 s |
最重要的是下面两个数据
- 新生代实际使用空间: 212,992 kb
- 老年代实际使用空间: 1,884,160 kb
因此,总的内存空间为2gb,不算perm空间的话,新生代与老年代之比为1:9。通过jstat和-verbosegc 日志进行数据收集,并把三台服务器按照如下方式设置。
- newratio=2
- newratio=3
- newratio=4
一天之后,检查系统的gc日志后发现,在设置了newratio参数后很幸运的没有发生full gc,
为什么?
- newratio=2: 45 ms
- newratio=3: 34 ms
- newratio=4: 30 ms
我们看到newratio=4 是最佳的参数,虽然它的新生代空间最小,但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执行频率下降。实际上,虽然full gc没有执行,但是minor gc被执行了 2,424次。
示例2
这是一个针对servicea的例子,我们通过公司内部的应用性能管理系统(apm)发现jvm暂停了相当长的时间(超过8秒),因此我们进行了gc优化。我们找到了full gc执行时间过长的原因,并着手解决。
进行gc优化的第一步,就是我们添加了-verbosegc参数,并得到如下结果。
图1:进行gc优化之前的stw时间
如上图所示,由hpjmeter自动生成的图片之一。x坐标表示jvm执行的时间。y坐标表示每次gc的时间。cms绿点,表示full gc结果。parallel scavenge蓝点,表示minor gc结果。
之前我曾经说过cms gc是最快的,但是上面的的结果显示出于某种原因,它最多花费了15秒。是什么导致这个结果?是否想起我之前提过的,cms在进行内存清理时,会变慢。与此同时,服务的内存被设定为 –xms1g和–xmx4g ,且实际分配了4gb内存。
因此,我将gc类型从cms改为parallel gc。并且将内存改为2gb,设定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
相对于4gb时的15秒,full gc变成了平均每次3秒。但是3秒一样比较慢,因此我设计了如下6种场景。
- case 1: -xx:+useparallelgc -xms1536m -xmx1536m -xx:newratio=2
- case 2: -xx:+useparallelgc -xms1536m -xmx1536m -xx:newratio=3
- case 3: -xx:+useparallelgc -xms1g -xmx1g -xx:newratio=3
- case 4: -xx:+useparalleloldgc -xms1536m -xmx1536m -xx:newratio=2
- case 5: -xx:+useparalleloldgc -xms1536m -xmx1536m -xx:newratio=3
- case 6: -xx:+useparalleloldgc -xms1g -xmx1g -xx:newratio=3
那一个最快呢?结果显示,内存越小,结果越好。下图展示了case6的结果。这是gc的性能最好。最长的响应时间只有1.7秒。平均时间在1秒之内。
图2:case6的时间图表
基于以上结果。我们按照case6调整了gc参数。但是,这导致了每天晚上都会发生outofmemoryerror。在这里很难解释具体的原因。简单来说,批处理程序导致了内存泄漏。相关的问题已经被解决。
如果对gc日志只分析很短的时间就贸然对所有服务器进行优化是非常危险的。请时刻牢记,你必须同时分析gc日志和应用程序。
我们回顾了两个关于gc优化的例子,正如我之前提到的,例子中提到的gc参数,可以设置在相同的服务器之上,但前提是他们具有相同的cpu,操作系统,jdk版本以及运行着相同的服务。但是不要直接把我用过的参数用到你的服务至上,它们未必能很好的工作。
结论
我凭借经验进行gc优化,而没有执行堆转储并分析内存的详细内容。精确地分析内存可以得到更好的优化效果。但是,这种分析一般适用于内存使用量相对固定的场合。不过,如果服务严重过载并占用的大量的内存,强力建议根据之前的经验进行gc优化。
我已经在一些服务上设置了g1 gc参数,并进行过性能测试。但还没有应用与正式环境,g1 gc参数的速度要快于其他任何gc类型。但是,你必须要升级到jdk7。另外,他的稳定性也暂时没有保障,没人知道是否会出现致命的错误。因此还不到将其正式应用的时候
在未来的某一天,等到jdk7真正稳定了(这不是说他现在不稳定),并且was针对jdk7进行优化后,g1 gc最终能够按照预期的那样工作了,我们可能就不需要在进行gc优化了。
想了解gc优化的更多内容,请登录slideshare.com 查看关联资源。强烈推荐everything i ever learned about jvm performance tuning @twitter。作者attila szegedi,一位twitter工程师。请花些时间阅读。
by sangmin lee, nhn performance engineering lab.
作者sangmin lee,就职于nhn性能工程研究院
英文原文:cubrid,编译:importnew-王晓杰
译文地址: http://www.importnew.com/3146.html
【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】
-- 扫描加关注,微信号: importnew --相关文章
- 成为java gc专家(4) — apache的maxclients参数详解及其在tomcat执行fullgc时的影响
- 成为javagc专家part ii — 如何监控java垃圾回收机制
- 成为javagc专家part i — 深入浅出java垃圾回收机制
- jvm 性能优化, part 4: c4 垃圾回收
- jvm性能优化, part 3 —— 垃圾回收
- 诊断java.lang.outofmemoryerror(oom)
- jvm垃圾回收机制
- 嵌套类:静态嵌套类和非静态嵌套类
- 常见java面试题 – 第一部分:非可变性(immutability)和对象引用(object reference)
- google i/o 2013大会andoird更新前瞻