记一次JVM调优日志(项目频繁GC,Full GC次数超标)

【前记】

最近公司的项目都统一接入了一个监控平台,当然...自己负责的项目也不能逃脱此厄运

由此,就发生了接下来一系列的 emmm... -_-"

因为项目一开始并不是我们自己研发的,而是找的第三方外包公司,后面就丢给了我。所以,你们懂的...

so...那接下来干嘛呢?那当然是一系列的优化咯

一、项目频繁GC,Full GC次数超标

通过观察我们发现Full GC次数明显超标,对于JVM垃圾回收,由于自己之前一直是处于理论阶段,就只是知道新生代、老年代的晋升关系,这些知识也仅能够对付面试。那对于这次服务器的FullGC频繁,一言不合就接口停滞。很明显应用状态明显不正常呗。怎么办?正所谓有困难就要迎难而上,对于我来说当然是一次非常难得的挑战机会咯,说干就干。

首先,我们先来看一下我们的jvm启动参数:

-Xms2000M -Xmx2000M -Xmn600M -Xss512K
-XX:+PrintGCDetails
-XX:+AggressiveOpts -XXUseBiasedLocking
-XX:+DisableExplicitGC -XX:MaxTenuringThreshold=15
-XX:+UseConMarkSweepGC -XX:UseParNewGC -XX:+CMSParallelRemarkEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=XXX
-Dfile.encoding=UTF8 -Dsun.jnu.encoding=UTF8 -Duser.timezone=Asia/Shanghai
-Djava.awt.headless=true
-Djava.security.egd=file:XXX
-jar.app.jar

在了解清楚jvm的内存分配情况以后,我们再来看一下服务器的基本配置和资源分配情况是否达到我们启动参数配置的单个pod的标准

单个pod资源限制:4096Mib

单个pod所需资源:2048Mib

显然两者匹配没有问题,我们接着往下走...接下来我尝试调整了jvm的核心启动参数:尝试提高jvm的初始化内存以及年轻代的大小。

  • -Xmx4000m:设置jvm最大可用内存为4000m。
  • -Xms4000m:设置jvm初始化内存为4000m,此值设置与-Xmx相同,以避免每次垃圾完成后jvm重新分配内存。
  • -Xmn2500m:设置年轻代大小为2500M。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
  • -Xss512m:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

贴一下调整后的启动参数:

-Xms4000M -Xmx4000M -Xmn2500M -Xss512K

就这样我们将启动参数配置部署到线上并修改服务器应用资源配置后,运行1天后,观察GC结果,YoungGC减少了一半以上的次数,时间减少了400s,但依然还是存在FullGC的情况。YoungGC基本符合预期设想,但是这个FullGC就完全不行。

 路漫漫其修远兮,吾将上下而求索呀...就这样第一次宣告失败。

故后续又做了一些调整如下:

  • -XX:SurvivorRatio=6:新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,此处我们改为6,也就是说Eden占新生代的6/10,From幸存区和To幸存区各占新生代的2/10。
  • -XX:MetaspaceSize=384m:jdk8及以后:设置元空间初始大小。
  • -XX:MaxMetaspaceSize=384m:jdk8及以后:设置元空间最大可分配大小。

最终我们的启动完整参数如下:

-Xms4000M -Xmx4000M -Xmn2500M -Xss512K
-XX:+PrintGCDetails
-XX:SurvivorRatio=6
-XX:MetaspaceSize=384m
-XX:MaxMetaspaceSize=384m
-XX:+AggressiveOpts -XXUseBiasedLocking
-XX:+DisableExplicitGC -XX:MaxTenuringThreshold=15
-XX:+UseConMarkSweepGC -XX:UseParNewGC -XX:+CMSParallelRemarkEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=XXX
-Dfile.encoding=UTF8 -Dsun.jnu.encoding=UTF8 -Duser.timezone=Asia/Shanghai
-Djava.awt.headless=true
-Djava.security.egd=file:XXX
-jar.app.jar

将SurvivorRatio修改为6的本意是想让垃圾在新生代时尽可能的多被回收掉。就这样我们又将配置部署到线上后,起初并没有出现FullGC的情况,这让我欣喜万分,本以为就这样圆满结束的。结果,令人奇怪的是,在运行了一周后,另一个周一早上,我百无聊赖的打开控制台,想看一下自己的成果时,突然发现FullGC次数在两天内竟突然激增,这时我才意识到,可能是自己的方向不对...

就这样第二次也宣告失败。

接下来我开始着手代码优化的问题,着手调查周末时间的定时任务中内存泄露的问题,由于我们并没有在线上添加FullGC前后生成dump文件(例如:-XX:+HeapDumpAfterFullGC)这个参数,那我只能去代码中逐个排查这个问题,最终我把问题锁定在了一个周末生成数据的定时任务上,接下来怎么办,那就在测试环境进行测试咯...说干就干!

删完数据并启动定时后,我如愿拿到了我想要的dump文件。

我们打开MAT开始分析dump文件,终于,抓到了内鬼!

 这个对象竟然有50多W条,可以确认这些数据是数据库查询或者插入时产生的了,所以我立刻去溯源,原来以前有个“大佬”在写一个查询的时候没有考虑到单表结果集数据过大的问题,索性能省则省,复用一个方法查询的时候省了一个条件,直接把表里面数据全部查出来。然后在用Stream流去对自己需要的数据进行过滤。emmm...看到这里,我心里一万句mmp从脑中闪过...至此,想必后面的代码优化方案也不用我多说了吧(还是说一下:建立相关索引,再新增mybatis plus条件字段查询)...至此,而且这也解释了为什么在某个时间段资源会激增,服务器会自动重启的原因。

解决完这个问题后,线上服务器运行完全正常。第三次调优成功!perfect!!!

总结

通过这次的调优总结出以下几点:

  • 发现FullGC频繁的时候优先调查内存泄漏问题。
  • FullGC频率过高异常需要重点关注。
  • 内存泄漏解决后,jvm可以调优的空间就比较少,切忌投入太多的时间。
  • 有必要时常关注服务器的GC,可以及早发现问题。

以上就是对本次JVM调优的过程记录,如有错误和更好的意见欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值