疫情下的电话
有一天阿洋打我电话,问我系统执行的效率比较慢,而且定时任务总是会出现延迟,短暂时间的停顿阻塞问题,问我能不能优化
系统优化
系统优化就是尽量的增加系统的性能和吞吐量,要想做到这两点必须需要系统优化,应该频繁的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 | java -XX:NewSize=838860800 -XX:MaxNewSize=838860800 |
新生代对象增长的速率 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 | java -XX:NewSize=800M -XX:MaxNewSize=800M -XX:InitialHeapSize=1024M |
改进
-XX:+DisableExplicitGC。这个参数的意思就是禁止显式执行GC,不允许你来通过代码触发GC
1 | java -XX:NewSize=800M -XX:MaxNewSize=800M -XX:InitialHeapSize=1024M -XX:MaxHeapSize=1024M -XX:SurvivorRatio=6 |
改进
在降低了Full GC频率之后,务必设置如下参数
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
每次Full GC后都整理一下内存碎片。
1 | java -XX:NewSize=800M -XX:MaxNewSize=800M |
改进
一个参数是“-XX:+CMSParallelInitialMarkEnabled”,这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行。
大家应该还记得初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少Stop the World的时间。
另外一个参数是“-XX:+CMSScavengeBeforeRemark”,这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。
其实大家都记得,CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
所以当时在JVM参数模板中,同样加入了这两个参数:
1 | java -XX:NewSize=800M -XX:MaxNewSize=800M |
而且调整了参数“-XX:CMSInitiatingOccupancyFraction=92”,避免老年代仅仅占用68%就触发GC,现在必须要占用到92%才会触发GC。
1 | java -XX:NewSize=800M -XX:MaxNewSize=800M |