服务器资源有限下的JVM调优

疫情下的电话

有一天阿洋打我电话,问我系统执行的效率比较慢,而且定时任务总是会出现延迟,短暂时间的停顿阻塞问题,问我能不能优化

系统优化

系统优化就是尽量的增加系统的性能和吞吐量,要想做到这两点必须需要系统优化,应该频繁的YoungGC、OldGC、FullGC会影响系统的性能和吞吐量,系统优化就是JVM优化,JVM优化就是分配合理的内存、参数设置,从而减少GC给系统带来的影响,想要解决这个问题,就必须了解JVM内存结构和GC的特点,不同的厂商JVM的实现不同,不同版本也存在差异;第二点就是没有绝对内存分配、参数设置的推荐值,必须结合系统的实际的运行模型,分析出系统比较占资源的核心业务,深入了解业务以及业务后续发展的规模做出评估,评估出QPS,以及每秒的内存占用情况,结合JVM的内存结构、GC特性来动态分配内存、参数设置从而尽可能的减少GC的次数,减少STW的时间;第三点就是就是很多时候机器资源是有限的,小公司更加看中成本,需要开发者自己去衡量,尽可能增加系统的性能,节约成本

实际线上分析

阿洋说核心业务主要是后台计算和第三方接口的数据同步,并不涉及到前端展示,服务器大概的情况是:单台服务器2核4G,上面运行了Mysql、Redis、Python,Java服务,2个Java程序,由于都是SpringBoot项目,说明有2个JVM进程,我分析了项目中最核心的一个功能点,就是同步人员人脸是否下发到考勤机上,单个项目最多有10000+人,有个最大的失误就是直接用集合容器存储,POJO有10个属性,其中5个属性有数据,算100字节吧,100*10000这就将近1M大小的实例了,随着后续人员的增加出现直接进入FullGC概率越来越大,考虑分批处理,粗略分析了下系统的运行模型,每秒占用的内存 10M,Eden600M,S1S2都60M,老年代400MB;每分钟都会MinorGC一次,剩余30%,180MB的对象不能回收,导致系统回收慢,卡顿是正常的;这边有个比较大的问题就是数据库查询出来的集合对象比较大,导致很多时候根本回收不了,加上服务器内存资源太有限了,得好好优化下

基础配置

阿洋的公司开发相对比较简单,之前都是使用默认参数,没有设置过JVM参数,而且2核4G的配置部署了其他非相关的进程,分配了1G给JVM Heap,下面是我的配置,由于程序不怎么设计到用户页面的交互,每次YGC SWT 的时间47ms是能够接受的,有一点没太明白就是每次YGC后存活下来的对象由50-60M的样子,在下次YGC触发的时候并不会动态年龄判断,OU的值一直是0;

1
2
3
4
5
java -XX:NewSize=838860800 -XX:MaxNewSize=838860800 
-XX:InitialHeapSize=1073741824 -XX:MaxHeapSize=1073741824
-XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=1048576 -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC -jar App.jar

新生代对象增长的速率 4m/s - Young GC的触发频率 1次/200ms - Young GC的耗时 47ms - 每次Young GC后有多少对象是存活下来的 50-60m - 每次Young GC过后有多少对象进入了老年代 0 - 老年代对象增长的速率 0 - Full GC的触发频率 0 - Full GC的耗时 257ms 在程序启动的时候触发4次

改进日志

1
2
3
4
5
java -XX:NewSize=800M -XX:MaxNewSize=800M -XX:InitialHeapSize=1024M 
-XX:MaxHeapSize=1024M -XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=1M -XX:CMSInitiatingOccupancyFraction=92
-XX:MetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+DisableExplicitGC -jar App.jar &

改进
-XX:+DisableExplicitGC。这个参数的意思就是禁止显式执行GC,不允许你来通过代码触发GC

1
2
3
4
5
java -XX:NewSize=800M -XX:MaxNewSize=800M -XX:InitialHeapSize=1024M -XX:MaxHeapSize=1024M -XX:SurvivorRatio=6  
-XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=1M
-XX:CMSInitiatingOccupancyFraction=92 -XX:MetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+DisableExplicitGC -jar App.jar &

改进
在降低了Full GC频率之后,务必设置如下参数
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
每次Full GC后都整理一下内存碎片。

1
2
3
4
5
6
7
java -XX:NewSize=800M -XX:MaxNewSize=800M 
-XX:InitialHeapSize=1024M -XX:MaxHeapSize=1024M -XX:SurvivorRatio=6
-XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=1M
-XX:CMSInitiatingOccupancyFraction=92 -XX:MetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
-jar App.jar &

改进
一个参数是“-XX:+CMSParallelInitialMarkEnabled”,这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行。
大家应该还记得初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少Stop the World的时间。

另外一个参数是“-XX:+CMSScavengeBeforeRemark”,这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。
其实大家都记得,CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。

所以当时在JVM参数模板中,同样加入了这两个参数:

1
2
3
4
5
6
java -XX:NewSize=800M -XX:MaxNewSize=800M 
-XX:InitialHeapSize=1024M -XX:MaxHeapSize=1024M
-XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=1M -XX:CMSInitiatingOccupancyFraction=92 -XX:MetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+DisableExplicitGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -jar App.jar &

而且调整了参数“-XX:CMSInitiatingOccupancyFraction=92”,避免老年代仅仅占用68%就触发GC,现在必须要占用到92%才会触发GC。

1
2
3
4
5
6
7
java -XX:NewSize=800M -XX:MaxNewSize=800M 
-XX:InitialHeapSize=1024M -XX:MaxHeapSize=1024M
-XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=1M -XX:CMSInitiatingOccupancyFraction=92 -XX:MetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+DisableExplicitGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:CMSInitiatingOccupancyFaction=92
-jar App.jar &