java GC 学习(五)

"JVM调优"

Posted by Jht on February 22, 2017

GC调优是必须的吗?

GC调优并非所有Java服务都必须做的事情。当然这是基于你已经使用了下面的选项或事实:

  • 通过 -Xms 和 -Xmx 选项指定了内存大小
  • 使用了 -server 选项
  • 系统未产生太多超时日志

也就是说,如果你未设置内存大小并且你的系统产生了过多的超时日志,恭喜你需要为你的系统执行GC调优。

如果应用的内存占用不断提升,你就要开始对其进行GC调优了。我把GC调优的目标分为以下两类:

  • 降低移动到老年代的对象数量
  • 缩短Full GC的执行时间

降低移动到老年代的对象数量

除了G1 GC外,其他的GC都是基于分代回收的。也就是对象会在Eden区中创建,然后不断在Survivor中来回移动。 之后如果该对象依然存活,就会被移到老年代中。有些对象,因为占用空间太大以致于在Eden区中创建后就直接移动到了老年代。 老年代的GC较新生代会耗时更长,因此减少移动到老年代的对象数量可以降低full GC的频率。 减少对象转移到老年代可能会被误解为把对象保留在新生代,然而这是不可能的,相反你可以调整新生代的空间大小 。

缩短Full GC耗时

Full GC的单次执行与Minor GC相比,耗时有较明显的增加。如果执行Full GC占用太长时间(例如超过1秒),在对外服务的连接中就可能会出现超时。

  • 如果企图通过缩小老年代空间的方式来降低Full GC执行时间,可能会面临 OutOfMemoryError 或者带来更频繁的Full GC。
  • 如果通过增加老年代空间来减少Full GC执行次数,单次Full GC耗时将会增加。

因此,需要 为老年代空间设置适当的大小 。

影响GC性能的选项

存活在新生代中但未变为不可达的对象会被复制到老年代。一般来说老年代的内存空间比新生代大, 所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为”major GC“(或者full GC)。

影响GC性能的选项

Java GC选项的设置时。设置很多选项未必能提高GC执行速度,相反还可能会更加耗时。 GC调优的基本规则是对两台或更多的服务器设置不同的选项,并对比性能表现 ,然后把被证明能提升性能的选项添加到应用服务器上。

表1: GC调优需要关注的选项

分类 选项 说明
堆空间 -Xms 启动JVM时的初始堆空间大小
col 2 is -Xmx 堆空间最大值
新生代空间 -XX:NewRatio 新生代与老年代的比例s
新生代空间 -XX:NewSize 新生代大小
新生代空间 -XX:SurvivorRatio Eden区与Survivor区的比例

再有就是GC类型

详见java GC 学习(四)-分代垃圾回收

GC调优过程

1.监控GC状态

首先需要监控GC状态信息以明确在GC操作过程中对系统的影响。

详见:

java GC 学习(二)-GC监控

java GC 学习(三)-开启GC日志

2. 分析监控数据并决定是否需要GC调优

然后通过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进程会中断, 因此不要在正常运行时系统上做此操作

3. 设置GC类型和内存大小

如果决定做GC调优,就需要考虑如何选择GC类型、如何设置内存大小。如果你有多台服务器, 可通过为每台服务器设置不同的GC选项并对比不同的表现,这一步很重要。

4. 分析GC调优结果

设置GC选项后,至少要收集24小时的GC表现数据,然后就可以着手分析这些数据了。如果足够幸运, 通过分析就刚好找到了最合适的GC选项。否则就需要分析GC日志,并分析内存的分配情况。 然后通过不同的调整GC类型和内存大小来找到系统的最优选项。

4.如果结果可接受,则对所有服务应用调优选项并停止调优

如果GC结果令人满意,就可以把相应的选项应用到所有服务器并停止GC调优。

监控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的频繁执行。

设置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更为耗时,也可能会带来其他问题。

调整内存大小

下面先列出内存大小与GC执行次数、每次GC耗时之间的关系:

  • 大内存
    • 会降低GC执行次数
    • 相应的会增加GC执行耗时
  • 小内存
    • 会缩知单次GC耗时
    • 相应的会增加GC执行次数

通常情况下,设置512MB大小。这不是说你要把自己的WAS(Web Application Server)内存选项设置为 -Xms512 和 -Xmx512m 。 基于当前未调优时的场景,检查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调优结果

通过HPJMeter分析GC日志。

分析过程中主要关注以下数据(按优先级排序):

  • Full GC(平均)耗时
  • Minor GC(平均)耗时
  • Full GC执行间隔
  • MinorGC执行间隔
  • Full GC整体耗时
  • Minor GC整体耗时
  • GC整体耗时
  • Full GC执行次数
  • Minor GC执行次数

heap size<=3G的情况下完全不要考虑CMS GC

heap size<=3G的情况下完全不要考虑CMS GC,在heap size>3G的情况下也优先选择ParallelOldGC, 而不是CMS GC,只有在暂停时间无法接受的情况下才考虑CMS GC(不过当然,一般来说在heap size>8G后基本上都得选择CMS GC, 否则那暂停时间是相当吓人的,除非是完全不在乎响应时间的应用),这其实也是官方的建议

为什么给了一个这么“武断”的建议呢,不是我对CMS GC有什么不爽, 相反CMS GC一直是我很热爱的一种GC实现,之所以建议在<=3G的情况下完全不要考虑CMS GC, 主要出于以下几点考虑:

  • 1、触发比率不好设置 在JDK 1.6的版本中CMS GC的触发比率默认为old使用到92%时, 假设3G的heap size,那么意味着旧生代大概就在1.5G–2.5G左右的大小,假设是92%触发, 那么意味着这个时候旧生代只剩120M–200M的大小,通常这点大小很有可能是会导致不够装下新生代晋生的对象, 因此需要调整触发比率,但由于heap size比较小,这个时候到底设置为多少是挺难设置的,例如我看过heap size只有1.5G, old才800m的情况下,还使用CMS GC的,触发比率还是80%,这种情况下就悲催了,意味着旧生代只要使用到640m就触发CMS GC, 只要应用里稍微把一些东西cache了就会造成频繁的CMS GC。 CMS GC是一个大部分时间不暂停应用的GC, 就造成了需要给CMS GC留出一定的时间(因为大部分时间不暂停应用,这也意味着整个CMS GC过程的完成时间是会比ParallelOldGC时的一次Full GC长的), 以便它在进行回收时内存别分配满了,而heap size本来就小的情况下,留多了嘛容易造成频繁的CMS GC,留少了嘛会造成CMS GC还在进行时内存就不够用了, 而在不够用的情况下CMS GC会退化为采用Serial Full GC来完成回收动作,这个时候就慢的离谱了。
  • 2、抢占CPU CMS GC大部分时间和应用是并发的,所以会抢占应用的CPU,通常在CMS GC较频繁的情况下, 可以很明显看到一个CPU会消耗的非常厉害。
  • 3、YGC速度变慢 由于CMS GC的实现原理,导致对象从新生代晋升到旧生代时,寻找哪里能放下的这个步骤比ParallelOld GC是慢一些的, 因此就导致了YGC速度会有一定程度的下降。
  • 4、碎片问题带来的严重后果 CMS GC最麻烦的问题在于碎片问题,同样是由于实现原理造成的, CMS GC为了确保尽可能少的暂停应用,取消了在回收对象所占的内存空间后Compact的过程, 因此就造成了在回收对象后整个old区会形成各种各样的不连续空间,自然也就产生了很多的碎片, 碎片会造成什么后果呢,会造成例如明明旧生代还有4G的空余空间,而新生代就算全部是存活的1.5g对象, 也还是会出现promotion failed的现象,而在出现这个现象的情况下CMS GC多数会采用Serial Full GC来解决问题。 碎片问题最麻烦的是你完全不知道它什么时候会出现,因此有可能会造成某天高峰期的时候应用突然来了个长暂停,于是就悲催了, 对于很多采用了类似心跳来维持长连接或状态的分布式场景而言这都是灾难,这也是Azul的Zing JVM相比而言最大的优势(可实现不暂停的情况下完成Compact, 解决碎片问题)。 目前对于这样的现象我们唯一的解决办法都是选择在低峰期主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题, 但这显然是一个很龌蹉的办法(因为同样会对心跳或维持状态的分布式场景造成影响)。
  • 5、CMS GC的”不稳定“性 如果关注过我在之前的blog记录的碰到的各种Java问题的文章(可在此查看), 就会发现碰到过很多各种CMS GC的诡异问题,尽管里面碰到的大部分BUG目前均已在新版本的JVM修复, 但谁也不知道是不是还有问题,毕竟CMS GC的实现是非常复杂的(因为要在尽可能降低应用暂停时间的情况下还保持对象引用的扫描不要出问题), 而ParallelOldGC的实现相对是更简单很多的,因此稳定性相对高多了。

而且另外一个不太好的消息是JVM Team的精力都已转向G1GC和其他的一些方面,CMS GC的投入已经很少了(这也正常,毕竟G1GC确实是方向)。

在大内存的情况下,CMS GC绝对是不二的选择,而且Java在面对内存越来越大的情况下,必须采用这种大部分时候不暂停应用的方式, 否则Java以后就非常悲催了,G1GC在CMS GC的基础上,有了很多的进步,尤其是会做部分的Compact, 但仍然碎片问题还是存在的

Java现在在大内存的情况下还面临的另外两个大挑战:

  1. 分析内存的堆栈太麻烦,例如如果在大内存的情况下出现OOM,那简直就是杯具, 想想dump出一个几十G的文件,然后还要分析,这得多长的时间呀,真心希望JDK在这方面能有更好的工具…
  2. 对象结构不够紧凑,导致在内存空间有很高要求的场景Java劣势明显,不过这也是新版本JDK会重点优化的地方。 至于在cpu cache miss等控制力度上不如C之类的语言,那是更没办法的,相比带来的开发效率提升,也只能认了, 毕竟现在多数场景都是工程性质和大规模人员的场景,因此开发效率、可维护性会更重要很多。

参考资料

GC调优实践

heap size<=3G的情况下完全不要考虑CMS GC