公司来了个大佬,把FullGC 40次/天优化为10天1次,太秀了~!
本文主体内容为转载文章,请查看如上原文
零,分析与思考
如果自己遇到这个问题如何去排查问题,找原因,定最终处理方案
上文中主要说的是FullGC,还没有达到内存溢出的程度,还没有拖挂服务。
把问题推到极致,服务内存溢出了
猜测
首先猜测原因,创建了大量的对象,导致内存溢出
排查
1,日志看报错内容 2,下载dump文件,分析到底是哪个对象创建过多
原因
最重要的事,是找到导致内容溢出原因,然后针对性的提出解决方案
一般原因:从数据库查询过多的,在for循环中new了大量对象
方案
找到是什么原因后,修改对应的代码,发布。可以从以下几方面考虑
1,控制数据量,增加条件,减少从数据库查询的数据量
2,控制并发量,如果频繁的调用一个会创建大量对象的接口,同一时间,方法没有执行完,对象没有释放,导致内存溢出
3,避免大事物,如果方法执行时间比较长,同样也增加同一时间创建大量对象的风险,要减少方法执行时间,对象及时释放掉。
处理大事物方案(接口性能优化方案)
1,索引,第一步慢sql,接口慢,首先考虑是sql慢,从优化慢sql角度优化,首先是加索引,避免使索引失效的写法,用explain调试一下
索引失效的原因:不符合最左匹配原则/使用了函数/左like/is null/or/字段数据类型不一致(join 关联字段)
1.1,慢sql,优化慢sql,比如要加索引及索引优化,且避免索引失效/避免三个以上join/select需要的/小表取消大表/批量操作/高效的分页/用join代替子查询/用union all代替union(没有重复记录或者允许重复存在,使用 UNION ALL
通常会更高效。
性能影响:
- 使用
UNION ALL
通常会提高查询性能,因为它省略了去重的计算过程。 UNION
的去重操作可能会在处理大量数据时引入额外的开销。
)
2,批量,尤其是跨服务调用,单个多次调用,封装批量接口
3,控制数据量,数据量一般是比较根本的原因,量降下来,可以避免很多问题
4,分批处理,尤其是查询,比如调用第三方接口/跨服务查询,分批查询,永辉超市一般一批2000
同步调用,for循环
异步调用,使用ComplateFuture
5,使用编程性事物,耗时的查询方法放在事物外
6,异步,不重要的逻辑,异步执行,比如调用第三方服务,写入日志
7,远程调用
7.1 并行调用
多个查询可以使用ComplateFuture并行查询(注意使用线程池),比如调用第三方接口查询,无法提供批量接口,只能单个查询。同时注意的是避免并发过高,控制每秒查询次数,具体多少可以与第三方沟通确认
7.2 数据异构
把多个数据源结构异构到一个结果中,比如查询用户信息,积分信息,库存信息,冗余到一个json中,存在redis,但是这样存在一个比较严重的问题,数据一致性问题,比如库存变了,所以使用时要充分考虑一致性问题
8,使用缓存,二级缓存 redis + 咖啡。注意设置过期时间/多个服务器缓存不一致问题
9,使用多线程,比如上面第7点,使用ComplateFuture多线程查询。或者for循环处理,单个逻辑比较耗时,可以使用多线程等等
-----------------------以下偏重接口性能优化---------
10,递归查询,避免死循环,通过控制调用层级
11,MQ
12,锁粒度,缩小粒度
12.1 sychronized在方案上加锁,改成代码块加锁,加在并发出现问题的那部分代码
12.2 分布式锁redis
13,分库分表
分库:着重解决,并发查询,数据库连接资源不足和磁盘IO瓶颈问题
分表:着重解决,单表查询性能问题和消耗cup资源问题
但是实际上有更优的方案,比如数据仓库,es
14,辅助功能
慢查询日志/skywalking/prometheus
总结
从文章内容分析
1,频繁FullGC过程及原因
FullGC是回收老年代内存,
为什么老年代内存多,频繁FullGC,是年轻代内容没有回收掉的,放到老年代。
或者对象频繁地从年轻代推送到老年代
为什么年轻代没有回收掉,young GC时,对象创建后,存在年轻代大量对象还在使用。或者高并发场景,同一个时间创建了大量对象
为什么没有回收的对象频繁的从年轻代推到老年代,年轻代内存比较小,很容易满
为什么大量对象长时间没有被回收掉,方法执行时间过长没有结束或者一段时间确实频繁创建了大量的对象
总结两点原因
1,年轻代内存,小
2,创建了大量的对象(高并发或者方法未执行完)
方案
1,增加年轻代内存大小,大了,可以减少young GC频率,young GC时,对象都用完了,方法已经结束了。就不会进入老年代了
2,找到没有被回收的对象,代码有优化,减少对象的创建,优化内存泄漏的地方
转载内容
今天给大家分享一个真实的jvm fullgc的优化案例,因为平时大家可能出去面试经常会问你们自己的jvm是如何优化的,但是现实的问题是,其实一般来说大家可能jvm都不需要优化,因为一般来说生产环境的系统设置jvm参数的时候,其实就关键就是把堆内存设置大一些,跟机器内存相匹配,然后就是设置一下垃圾回收器就可以了。
然后大家可以思考一下线上系统跑起来以后jvm的工作过程,系统在不断运行的过程中,咱们写的代码不断的执行,那无非就是不断的创建对象,然后基于对象去执行各种业务逻辑和数据库crud对吧,然后这些对象如果大家想想的话,一般都是在controller、service、dao等单例类的方法里创建的,所以基本都是短生命周期的对象,方法执行完毕,对象其实就可以回收了,这个大家没问题吧?
那所以其实对于jvm来说,你就是方法里不断的创建对象扔到jvm堆里去,进jvm堆的新生代,然后就是等着新生代满了以后就触发一次young gc,把新生代里不用的对象回收掉,其实这个时候大部分的对象都随着方法执行完毕就可以回收了,那其实所以一般很少对象会进入老年代里去,除非是spring管理的那些单例对象,类似你的controller、service、dao这些。
所以大部分的系统运行的时候其实用默认的年轻代垃圾回收器+老年代CMS垃圾回收器+jvm堆设置大一些+年轻代和老年代比例调整合理一些,就ok了,一般来说其实young gc就是固定频率执行一次,很少对象进老年代,fullgc一般不触发,或者很少触发,这其实就是大部分系统的真实情况,所以jvm一般其实不用我们太多操心和优化!
但是其实对于jvm在特殊情况下还是会出现性能问题的,那对于jvm的优化我们还是需要掌握一些案例的,就是在各种情况下如何排查和分析jvm频繁fullgc的问题,这个是关键,因为一个出去面试经常要问,一个是万一以后你遇到一个不太一样的系统,确实jvm真就出问题了呢!比如频繁fullgc,fullgc会引起stop the world,jvm会停顿,系统会感觉经常卡慢,那万一你遇到不还得优化?
所以啊,今天讲的这个案例分析,还是值得大家看看,一个出去面试可以聊,一个积累点jvm排查分析优化的经验思路,以后遇到自己也能上手!
那今天就给大家讲述一个真实案例,讲述我们的一个小伙伴如何将线上服务器的FullGC(完全垃圾回收)次数从每天40次优化到每10天才触发一次,极大地提升了系统性能和稳定性。
一、问题的初现
前一段时间,我们公司的线上服务器遇到了一个棘手的问题——FullGC非常频繁,平均每天高达40多次。不仅如此,服务器还隔三差五地自动重启,严重影响了业务的正常运行。这种情况显然不正常,我们团队紧急召开了会议,决定主动出击,寻找问题的根源并进行优化。
首先,我们查看了服务器的配置,发现配置相当一般:2核4G,总共4台服务器组成集群。每台服务器的FullGC次数和时间都基本相近,这表明问题并非个例,而是普遍存在的。接着,我们分析了JVM的几个核心启动参数:
-
-Xms1000M
:设置JVM初始化内存为1000M。 -
-Xmx1800M
:设置JVM最大可用内存为1800M。 -
-Xmn350M
:设置年轻代大小为350M。 -
-Xss300K
:设置每个线程的堆栈大小为300K。 -
-XX:+UseParNewGC
:使用并行新生代垃圾收集器。 -
-XX:+UseConcMarkSweepGC
:使用并发标记清除老年代垃圾收集器。 -
-XX:CMSInitiatingOccupancyFraction=70
:设置CMS垃圾收集在老年代占用率达到70%时启动。大家看着没有,上面的jvm参数起始没啥特别的,一般生产系统上线都这么玩,设置一下堆内存大小、指定一下垃圾回收器就ok了,然后等着系统跑吧,一般都没啥事儿,这里说一下,一般G1垃圾回收器适合对垃圾回收时间特别敏感的那种,就是可以适当频繁垃圾回收,但是每次时间不能长,不能影响系统运行和卡顿。
这种一般适合于那种互联网高并发系统,因为高并发会导致对象生产速度过快,很多时候对象背后的方法还没执行完,然后年轻代就满了,这个时候对象会快速进入老年代,然后频繁的触发fullgc,这个时候其实依靠ParNew+CMS就不太合适了,因为哪怕你设置很大的堆内存,那其实容纳对象越多,每次触发gc耗时越长。
所以对于互联网高并发线上系统,其实一般都可以用G1垃圾回收,精准控制垃圾回收时间,让G1自己控制垃圾回收频率和回收时间,避免系统卡顿。
那对于这次的系统来说,这些参数看起来并无明显异常,但仔细分析后,我们发现了几个问题:
1、新生代太小:新生代只有350M,对于我们的应用来说可能偏小,容易导致YoungGC频繁触发,每次触发的时候可能很多对象还没用完,可能会导致很多对象不断进入老年代,老年代频繁塞满,自然会导致一天几十次fullgc了。
2、堆内存初始化与最大值不一致:-Xms和-Xmx设置不一致,可能会导致JVM在垃圾回收后重新分配内存,影响性能,这是一个次要因素,其实一般都不建议生产系统xms和xmx设置不同。
二、第一次优化尝试
基于上述分析,我们决定进行第一次优化尝试:
1、提升新生代大小:将-Xmn从350M增加到800M,期望减少YoungGC的次数和时间,增加年轻代大小以后,那对象容纳就多了,younggc频率肯定减少,每次younggc的时候里面大部分对象应该都用完了,就可以回收了,那就可以减少进老年代的对象数量了,fullgc应该会减少。
2、设置初始化堆内存与最大堆内存一致:将-Xms设置为1800M,避免JVM在每次GC后重新分配内存。
我们将这些配置部署到了线上两台服务器(prod和prod2,另外两台保持不变以便对比)上,运行了5天后观察GC结果:
1、YoungGC次数减少了一半以上,时间也减少了400秒,符合预期,毕竟空间大了,频率肯定会降低。
2、但令人失望的是,FullGC次数还是好几十次那么多,不知道为什么每次younggc完了以后还是有很多对象进入老年代,老年代频繁赛满的次数变多了,导致频繁fullgc,那基本就算是完蛋了。
这次优化宣告失败,YoungGC虽然有所改善,但FullGC问题却变得更加严重。
三、内存泄漏的发现与解决
所以要解决的关键问题是,为啥每次younggc以后都有很多对象进老年代,是内存泄露了吗?就是代码里不断创建某个对象,这个对象还被引用无法释放,然后就占用老年代空间,导致了频繁fullgc?在优化过程中,我们发现了一个关键线索:在代码里,某个对象T在内存中有一万多个实例,占据了近20M内存。通过项目代码审查,我们找到了内存泄漏的原因:
public void doSomething(T t) { component.addListener(new Listener() { public void success() { if (t.success()) { // ... } } }); }
上面代码的问题就是随着代码运行,不断的创建对象T以及对应的component组件里的监听器,一个监听器对应一个对象T,然后正常来说其实应该是每次success回调过后,就完成处理,然后把监听器和T对象都释放掉,可是代码里没做这个动作,就导致T对象和监听器不断积压进入老年代,导致了频繁fullgc。
我们修复了这个问题,首先排查并解决了程序中的所有success监听回调事件,每次回调处理完后一定要释放T对象和监听器对象,然后重新发布应用,这样的话就可以尽量在youngc回收掉这部分对象,即使进老年代,那老年代也可以回收掉,不至于一直积压在老年代里。
然而,fullgc虽然次数减少了,每天没有几十次fullgc了,基本也就偶尔才一次fullgc,但是这个时候还是有一个问题,那就是系统每隔一段时间就会内存溢出oom一下,导致系统崩溃,需要去重启。
四、深入调查与dump内存
那现在fullgc问题已经解决了,其实就是代码里对象没释放导致内存泄露的问题,那针对oom问题,我们继续去调查问题,看是不是还有别的内存泄露问题,是不是某个对象一直无法释放导致oom。
代码排查效率不高,于是我们在系统不繁忙时进行了内存dump,终于抓到了一个巨大的对象:4万多个ByteArrowRow对象,这些对象显然是数据库查询或插入时产生的。
进一步分析日志和代码,运维同事发现某时段入口流量异常高涨,达到83MB/s,远超正常业务量。咨询阿里云客服后确认流量正常,排除攻击可能。最终,另一位同事找到了问题的根源:在某个条件下,查询未处理数据时没有加上模块条件,导致查询出40多万条记录。这些记录部分在内存dump时已被处理,但剩余部分仍在传输中,从而引发内存溢出和服务器重启。
那针对这个问题,其实也很简单,代码里加入查询条件,避免有时候一下子查出来40多w条数据就可以了。
五、最终优化与成效
解决了oom和内存泄漏问题后,服务器运行恢复了正常。我们发现这个时候FullGC次数显著减少,3天内只触发了5次。
接下来,我们继续优化JVM参数。查看GC日志发现,前三次FullGC时老年代内存占用率不足30%,却触发了FullGC。经过调查,我们怀疑是Metaspace(元空间)导致的FullGC。默认Metaspace大小为21M,而GC日志显示最大时达到了200M左右。于是,我们进行了如下调优:
-
将
-Xmn
设置为800M和600M(两台服务器不同配置以便对比)。 -
设置
-Xms
和-Xmx
均为1800M。 -
设置
-XX:MetaspaceSize=200M
,增加元空间初始大小。 -
调整
-XX:CMSInitiatingOccupancyFraction=75
,提高CMS启动阈值。优化后,metaspace空间变大了,那fullgc触发频率就很低了,而且cms触发阈值也调高了,所以线上服务器运行了10天左右,FullGC次数显著降低,且YoungGC次数和时间也进一步减少。最终,FullGC从每天40多次优化到每10天才触发一次,YoungGC时间也减少了一半以上。
六、总结与反思
这次优化过程让我们深刻认识到,FullGC频繁触发往往与内存泄漏、JVM参数设置不当等问题密切相关。通过逐步排查、dump内存、分析日志等手段,我们最终定位并解决了问题。同时,这次经历也提醒我们,日常运维中应时常关注服务器的GC情况,以便及早发现问题并进行优化。
通过这次优化,我们不仅提升了系统性能,还增强了团队的技术实力和协作能力。未来,我们将继续深入学习JVM垃圾回收机制,不断优化系统配置,确保业务稳定运行。