“这是我参与更文挑战的第11天,活动详情查看: 更文挑战”
上篇介绍了Java JVM性能优化案例实操上篇,接下来介绍Java JVM性能优化案例实操下篇,在介绍前,先探讨下新生代与老年代的比例。
一、新生代与老年代的比例
Eden、S0、S1的比例真的是8:1:1吗?
参数设置
JVM 参数设置为:
# 打印日志详情 打印日志打印日期 初始化内存300M 最大内存300M 日志路径
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms300M -Xmx300M -Xloggc:log/gc.log
复制代码
新生代 ( Young ) 与老年代 ( Old ) 的比例为 1:2,所以,内存分配应该是新生代100M,老年代 200M
我们可以先用命令查看一下堆内存分配是怎么样的:
# 查看进程ID
jps -l
# 查看对应的进程ID的堆内存分配
jmap -heap 14160
复制代码
我们可以看到堆内存配置的最大值MaxHeapSize = 300,新生代与老年代的比例NewRatio = 2,Eden、S0、S1的比例SurvivorRatio = 8,即 Eden:S0:S1 = 8:1:1
但实际Eden:S0:S1 = 8:1:1么?如下图所示
结果大家可以看到:我们的SurvivorRatio= 8 但是内存分配却不是8:1:1,这是为什么呢?
Eden:S0 = 75:12.5 = 6:1,是不是很神奇,莫急,听我慢慢道来!!!
参数AdaptiveSizePolicy
这是因为JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy,会根据GC的情况自动计算计算 Eden、From 和 To 区的大小;所以这是由于JDK1.8的自适应大小策略导致的,除此之外,我们下面观察GC日志发现有很多类似这样的FULLGC(Ergonomics),也是一样的原因。
我们可以在jvm参数中配置开启和关闭该配置:
# 开启:
-XX:+UseAdaptiveSizePolicy
# 关闭
-XX:-UseAdaptiveSizePolicy
复制代码
接下来我们关闭自适应进行测试是不是Eden:S0:S1 = 8:1:1了么?
如果不想动态调整内存大小,以下是解决方案:
- 保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。
2. 使用 CMS 垃圾回收器。CMS 默认关闭 AdaptiveSizePolicy。配置参数 -XX:+UseConcMarkSweepGC。
注意事项
- 在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false;不过不同版本的JDK存在差异;
UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;
- 由于UseAdaptiveSizePolicy会动态调整 Eden、Survivor 的大小,有些情况存在Survivor 被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。
对于面向外部的大流量、低延迟系统,不建议启用此参数,建议关闭该参数。
二、CPU占用很高排查方案
案例
死锁问题,相信大家都知道,就不写案例了,自行脑补,说白了,就是互相等待对方手里持有的锁,我中有你,你中有我,僵持不下,干瞪眼!!!
问题分析
如果线程死锁,那么线程一直在占用CPU,这样就会导致CPU一直处于一个比较高的占用率。所示我们解决问题的思路应该是:
- 首先查看java进程ID
- 根据进程 ID 检查当前使用异常线程的pid
- 把线程pid变为16进制如 31695 -> 7bcf 然后得到0x7bcf
- jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码
接下来是我们的实现上面逻辑的步骤,如下所示:
# 查看所有java进程 ID
jps -l
复制代码
结果如下:
根据进程 ID 检查当前使用异常线程的pid
top -Hp 1456
复制代码
结果如下:
从上图可以看出来,当前占用cpu比较高的线程 ID 是1465
接下来把 线程 PID 转换为16进制为
# 10 进制线程PId 转换为 16 进制
1465 -------> 5b9
# 5b9 在计算机中显示为
0x5b9
复制代码
jstack 1465 | grep -A20 0x5b9
解决方案
- 调整锁的顺序,保持一致。
- 或者采用定时锁,一段时间后,如果还不能获取到锁就释放自身持有的所有锁。
三、G1并发执行的线程数对性能的影响
配置信息
硬件配置:8核
JVM参数设置
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=1"
复制代码
初始化内存和最大内存调整小一些,目的发生 FullGC,关注GC时间
关注点是:GC次数,GC时间,以及 Jmeter的平均响应时间
初始的状态
启动tomcat,查看进程默认的并发线程数:jinfo -flag ConcGCThreads pid
-XX:ConcGCThreads=1
没有配置的情况下:并发线程数是1
查看线程状态:jstat -gc pid
得出信息:
YGC:youngGC次数是1259次
FGC:Full GC次数是6次
GCT:GC总时间是5.556s
复制代码
Jmeter压测之后的GC状态:
得出信息:
YGC:youngGC次数是1600次
FGC:Full GC次数是18次
GCT:GC总时间是7.919s
复制代码
由此我们可以计算出来压测过程中,发生的GC次数和GC时间差
压测过程GC状态:
YGC:youngGC次数是 1600 - 1259 = 341次
FGC:Full GC次数是 18 - 6 = 12次
GCT:GC总时间是 7.919 - 5.556 = 2.363s
复制代码
Jmeter压测结果如下:
压测结果如下:
主要关注响应时间:
95%的请求响应时间为:16ms
99%的请求响应时间为:28ms
复制代码
优化之后
增加线程配置:
export CATALINA_OPTS=”$CATALINA_OPTS -XX:ConcGCThreads=8″
观察GC状态
jstat -gc pid
tomcat启动之后的初始化GC状态:
总结:
YGC:youngGC次数是 1134 次
FGC:Full GC次数是 5 次
GCT:GC总时间是 5.234s
复制代码
Jmeter压测之后的GC状态:
总结:
YGC:youngGC次数是 1347 次
FGC:Full GC次数是 16 次
GCT:GC总时间是 7.149s
由此我们可以计算出来压测过程中,发生的GC次数和GC时间差
压测过程GC状态:
YGC:youngGC次数是 1347 - 1134 = 213次
FGC:Full GC次数是 16 - 5 = 13次
GCT:GC总时间是 7.149 - 5.234 = 1.915s 提供了线程数,使得用户响应时间降低了。
压测结果如下:
主要关注响应时间:
95%的请求响应时间为:15ms
99%的请求响应时间为:22ms
复制代码
建议
配置完线程数之后,我们的请求的平均响应时间和GC时间都有一个明显的减少了,仅从效果上来看,我们这次的优化是有一定效果的。大家在工作中对于线上项目进行优化的时候,可以考虑到这方面的优化。
四、总结
本节开头介绍了新生代中Eden、S0、S1的比例,大家都知道是8:1:1,但默认不是这个比例,影响因素有垃圾回收器、自适应调整新生代堆内存参数UseAdativeSizePolicy、SurvivorRatio、都有关系。其次介绍了CPU占用很高排查方案、G1并发执行的线程数对性能的影响的案例,希望能对大家有所帮助!
欢迎大家关注公众号(MarkZoe)互相学习、互相交流。