JVM调优入门(二):实战调优Parallel收集器

在调优JVM的时候,我们的目的是在一定的运行环境下提高 吞吐量 ,降低 最大停顿时间 。这篇文章以Parallel收集器来进行一次调优实战。

测试环境:青云上海1区A - 性能型 - ubuntu 16.04 - 2核12G

我们要调的是什么?

本文就以我们一个项目的启动速度极慢的Jar包为动手目标,将提升启动速度为目的,那是不太可能的,因为GC的速度本来早就已经优化的很快了,所以提升启动速度的效果不会明显。那我们要调的,要优化的到底是什么?

优化JVM垃圾收集性能从而增大吞吐量或减少停顿时间,让应用在某个业务场景上发挥最大的价值

这是我对JVM调优一个定义,在本文里,将以一个项目的启动过程模拟一个比较消耗资源的Web请求的过程,以 减少最大单个GC停顿时间在web应用中减少单个请求停顿时间 为目的来进行调优。

你可能有个疑问,前面不是已经提到,并行(Parallel)收集器适用于计算、后台处理这样的弱交互场景而不是web交互场景。但是我们为什么要用这个收集器来减少停顿时间而不是用CMS或G1收集器呢?因为Parallel有个特点,它支持基于行为的自适应调整,以及其他收集器都不支持的两个参数: -XX:MaxGCPauseMillis=<n>-XX:GCTimeRatio=<n> ,从而能精确控制吞吐量与停顿时间,且能自动调整堆的大小,所以虽然它不是偏向减少停顿时间的,但它的表现会更加稳定且可控,也是更加偏向业务场景选择的,所以这个收集器也是很有用的。

套路

其实JVM调优的套路非常简单,只需要以下三步即可:

  1. 拿到GC日志
  2. 分析日志
  3. 调整JVM参数

重复2和3,直到表现令你满意为止。但是实际上第2步是最考验技术实力的一步,你必须要对JVM内存结构、各种垃圾收集器调优的方式、甚至调优经验有一定的积累才能做好,否则将JVM调坏都有可能。本文也将重点介绍如何调整JVM参数以及为什么。

调试时一定要注意本地机器的内存大小是否够用,同时使用 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize 查看JVM默认最大堆以及其他最大值来避免影响调试

拿到GC日志

运行Java程序时添加以下参数以输出gc日志 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -verbose:gc -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:/tmp/gc.log

测试时运行的命令为 nohup java *JVM参数* -jar xxx.jar &

期间,使用 jps -ml 可以方便的拿到进程PID,从而 kill

3144 sun.tools.jps.Jps -ml
2959 cmp-managerServer.jar

最后,将 /tmp/gc.log 拿到本地,方便查看

第一次分析日志

有一个在线可视化工具方便查看, http://gceasy.io/

将刚生成的log上传上去,拿到结果:

可以看出:

调优之前原始的指标:

吞吐量 最大GC时间 最大Young GC时间 每次GC平均时间 Young GC次数 Full GC次数
96.759% 660ms 160 ms 36 ms 30 3
  1. 【JVM Heap Size】:年轻代的峰值接近于最大值
  2. 提示 49.47% of GC time (i.e 860 ms) is caused by 'Metadata GC Threshold'. 有一半的时间因为元空间不足导致的GC,且最久的一次GC也是元空间导致的,但是页面上显示元空间分配的是 1.05 gb ,讲道理不会发生不足的情况,自己使用 java -XX:+PrintFlagsFinal -version | grep MetaspaceSize 查看到默认元空间大小为20.796875MB
  3. 【Interactive Graphs】:【Young Gen】因为内存不足GC和扩容了很多次
  4. 【GC Causes】:有一半触发GC的原因是Young区不够
  5. GC时间最大值与Full GC次数太久,需要降低

所以我们可以下一个结论, 年轻代与元空间的初始空间不足导致频繁GC ,那么是不是增加年轻代和元空间的大小就能减少GC次数呢?是的,但是会带来其他问题。

年轻代越大,确实可以让Young GC减少,但是对于有限的堆大小来说,较大的年轻代意味着较小的老年代,这将增加其GC频率。并且如果年轻代很大,每次的Young GC时间也会增大因为需要GC的内容变多。所以最佳选择取决于应用程序分配的对象的生命周期分布

所以调整年轻代需要这样来:

  1. 先确定能提供给JVM的最大堆内存大小,并固定,注意不要超过系统内存避免使用文件交换
  2. 保持老年代足够大,以容纳应用程序在任何给定时间使用的所有实时数据,并加上一些缓冲区(10到20%或更多)
  3. 反复调整年轻代以满足你的需求,注意避免因年轻代太大导致Young GC时间长

第一次调整JVM参数

根据上面的分析,我们要把老年代设置一个比较大的空间,但最大堆内存和老年代的空间都是够用的,所以不需要额外的限制初始化老年代空间是多少,通过自动扩容是能够满足应用程序的。

将新生代的最大值调小到256MB通过增加Young GC来减少每次GC的数据量从而减少最大停顿时间: -XX:MaxNewSize=268435456

再根据 元空间调优的建议 ,将元空间大小扩大到96MB以适应第一次加载数据的空间需求,也能减少因为元空间导致的频繁GC: -XX:MetaspaceSize=100663296

第二次分析日志

吞吐量 最大GC时间 最大Young GC时间 每次GC平均时间 Young GC次数 Full GC次数
97.554% 570ms 60ms 16 ms 89 1
  1. GC平均时间下降是因为新生代减小,Young GC次数提升,每次回收的数据减少导致,当然Young GC的最大时间也会减少。 对于新生代来说,满足了减少单个GC停顿时间 ,能保证大部分web请求来的时候能有比较稳定的表现
  2. 但有一次花了570ms的Full GC,可能会导致某些web请求卡顿570ms,是Ergonomics导致

Ergonomics是Java虚拟机(JVM)和垃圾收集调优的过程,是Parallel收集器特有的自适应调优机制,我们只需要使用 Behavior-Based Tuning 配置两个参数即可。由于这个原因导致的Full GC我们先不解决,先尽量减少Young GC的最大停顿时间来继续优化新生代的最大GC时间

第二次调整JVM参数

使用 java -XX:+PrintFlagsFinal -version | grep GCTimeRatio 查看到默认值是99,也就是默认将垃圾回收的时间设置成了总时间的1%,能达到非常高的吞吐量的效果,但并不利于减少最大停顿时间,所以我们可以加上: -XX:GCTimeRatio=0 ,允许最低的吞吐量。

同样能看到最大停顿时间的默认值是无限制,所以我们需要设置一个最大停顿时间,以尝试使垃圾收集暂停时间小于 毫秒。这样调整可能会导致GC更频繁地发生,从而降低了应用程序的整体吞吐量。所以加上 -XX:MaxGCPauseMillis=10 。但这个参数无法限制Ergonomics调节堆大小和Full GC花费的时间。

两者的优先级是最大停顿时间高于吞吐量,也就是说先满足最大停顿时间,再满足吞吐量,所以可能会出现吞吐量无法满足的情况。

第三次分析日志

吞吐量 最大GC时间 最大Young GC时间 每次GC平均时间 Young GC次数 Full GC次数
95.011% 670ms 30ms 5 ms 593 1
  1. 为了满足最大停顿时间,吞吐量下降
  2. 为了满足最大停顿时间,新生代被限制到最大为90MB,无视掉了之前调优选择的新生代的最大值256MB。导致Young GC次数大大增加,即使最大Young GC时间减少到了30ms,但是次数却增多了将近10倍,所以不能达到整体上减少最大停顿时间的要求。
  3. 耗费670ms的Full GC依然存在。从老年代GC图中可以发现,可能是老年代的峰值达到了顶峰,然后触发的Ergonomics引起的Full GC,随后老年代的空间被扩大了。所以是因为老年代的初始空间不足导致的老年代的Full GC以及动态调整老年代大小。

第三次调整JVM参数

将新生代的初始(最小)大小设置为256MB避免因Ergonomics设置太小导致GC与扩容次数太多: -XX:NewSize=268435456 将老年代的初始(最小)大小设置为150MB避免因Ergonomics设置太小导致GC与扩容次数太多: -XX:OldSize=157286400

最后一次分析日志

吞吐量 最大GC时间 最大Young GC时间 每次GC平均时间 Young GC次数 Full GC次数
98.49% 40 ms 40ms 12 ms 73 0

最终的调优结果:

  1. 虽然我们设置的最大Young GC时间为10ms,但实际上由于我们添加了最小的新生代与老年代的限制,只能到40ms,不过比原来的Young GC最大时间160ms好太多了。进一步满足了 减少单个GC停顿时间 的需求
  2. Young GC次数是原来的2倍多,对于某个web请求来说,可能会多经历一个Young GC的时间,但现在的Young GC的平局时间是原来的1/3,所以还是有一定提升的。满足了 在web应用中减少单个请求停顿时间 的需求
  3. 最大GC时间从原来的570ms到30ms,还要归功于通过避免动荡的堆空间导致的Full GC以及堆空间调整

看起来效果很明显,但是这并不一定是最佳的参数配置,你可以试一下其他内存大小值或者使用一些更高级的参数继续调试。

在最后一次调整中,我们是通过避免Full GC来大大减少最大GC时间的,但实际上Full GC是不可避免的,并且每次耗时会在一百毫秒以上。即时在这次请求中没有发生Full GC,那么在下次就可能发生了,所以,我们还需要控制一下Full GC的执行次数与执行时间,可以参照 这几个建议 来:

-XX:PetenureSizeThreshold

总结与建议

  • 细心。要对各种数据、调优参数比较敏感
  • 耐心。别看我只写了调整三次,其实有三十次
  • 控制变量法,每次只调一个参数,别学我
  • 一般我们不需要像本文中一样精确的去优化,写上 -Xmx1g -Xms256m 就行,如果出现GC的性能问题再去调优
  • 记得复习一下GC调优的套路
  • oracle官方调优文档美团调优实战

号外号外

最近在总结一些针对 Java 面试相关的知识点,感兴趣的朋友可以一起维护~

地址: https://github.com/xbox1994/2018-Java-Interview

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章