在调优JVM的时候,我们的目的是在一定的运行环境下提高 吞吐量 ,降低 最大停顿时间 。这篇文章以Parallel收集器来进行一次调优实战。
测试环境:青云上海1区A - 性能型 - ubuntu 16.04 - 2核12G
本文就以我们一个项目的启动速度极慢的Jar包为动手目标,将提升启动速度为目的,那是不太可能的,因为GC的速度本来早就已经优化的很快了,所以提升启动速度的效果不会明显。那我们要调的,要优化的到底是什么?
这是我对JVM调优一个定义,在本文里,将以一个项目的启动过程模拟一个比较消耗资源的Web请求的过程,以 减少最大单个GC停顿时间 和 在web应用中减少单个请求停顿时间 为目的来进行调优。
你可能有个疑问,前面不是已经提到,并行(Parallel)收集器适用于计算、后台处理这样的弱交互场景而不是web交互场景。但是我们为什么要用这个收集器来减少停顿时间而不是用CMS或G1收集器呢?因为Parallel有个特点,它支持基于行为的自适应调整,以及其他收集器都不支持的两个参数: -XX:MaxGCPauseMillis=<n>
和 -XX:GCTimeRatio=<n>
,从而能精确控制吞吐量与停顿时间,且能自动调整堆的大小,所以虽然它不是偏向减少停顿时间的,但它的表现会更加稳定且可控,也是更加偏向业务场景选择的,所以这个收集器也是很有用的。
其实JVM调优的套路非常简单,只需要以下三步即可:
重复2和3,直到表现令你满意为止。但是实际上第2步是最考验技术实力的一步,你必须要对JVM内存结构、各种垃圾收集器调优的方式、甚至调优经验有一定的积累才能做好,否则将JVM调坏都有可能。本文也将重点介绍如何调整JVM参数以及为什么。
调试时一定要注意本地机器的内存大小是否够用,同时使用 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
查看JVM默认最大堆以及其他最大值来避免影响调试
运行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 |
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 所以我们可以下一个结论, 年轻代与元空间的初始空间不足导致频繁GC ,那么是不是增加年轻代和元空间的大小就能减少GC次数呢?是的,但是会带来其他问题。
年轻代越大,确实可以让Young GC减少,但是对于有限的堆大小来说,较大的年轻代意味着较小的老年代,这将增加其GC频率。并且如果年轻代很大,每次的Young GC时间也会增大因为需要GC的内容变多。所以最佳选择取决于应用程序分配的对象的生命周期分布
所以调整年轻代需要这样来:
根据上面的分析,我们要把老年代设置一个比较大的空间,但最大堆内存和老年代的空间都是够用的,所以不需要额外的限制初始化老年代空间是多少,通过自动扩容是能够满足应用程序的。
将新生代的最大值调小到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 |
Ergonomics是Java虚拟机(JVM)和垃圾收集调优的过程,是Parallel收集器特有的自适应调优机制,我们只需要使用 Behavior-Based Tuning 配置两个参数即可。由于这个原因导致的Full GC我们先不解决,先尽量减少Young GC的最大停顿时间来继续优化新生代的最大GC时间
使用 java -XX:+PrintFlagsFinal -version | grep GCTimeRatio
查看到默认值是99,也就是默认将垃圾回收的时间设置成了总时间的1%,能达到非常高的吞吐量的效果,但并不利于减少最大停顿时间,所以我们可以加上: -XX:GCTimeRatio=0
,允许最低的吞吐量。
同样能看到最大停顿时间的默认值是无限制,所以我们需要设置一个最大停顿时间,以尝试使垃圾收集暂停时间小于 -XX:MaxGCPauseMillis=10
。但这个参数无法限制Ergonomics调节堆大小和Full GC花费的时间。
两者的优先级是最大停顿时间高于吞吐量,也就是说先满足最大停顿时间,再满足吞吐量,所以可能会出现吞吐量无法满足的情况。
吞吐量 | 最大GC时间 | 最大Young GC时间 | 每次GC平均时间 | Young GC次数 | Full GC次数 |
---|---|---|---|---|---|
95.011% | 670ms | 30ms | 5 ms | 593 | 1 |
将新生代的初始(最小)大小设置为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 |
最终的调优结果:
看起来效果很明显,但是这并不一定是最佳的参数配置,你可以试一下其他内存大小值或者使用一些更高级的参数继续调试。
在最后一次调整中,我们是通过避免Full GC来大大减少最大GC时间的,但实际上Full GC是不可避免的,并且每次耗时会在一百毫秒以上。即时在这次请求中没有发生Full GC,那么在下次就可能发生了,所以,我们还需要控制一下Full GC的执行次数与执行时间,可以参照 这几个建议 来:
-XX:PetenureSizeThreshold
-Xmx1g -Xms256m
就行,如果出现GC的性能问题再去调优 最近在总结一些针对 Java 面试相关的知识点,感兴趣的朋友可以一起维护~
地址: https://github.com/xbox1994/2018-Java-Interview