百万级java后台生产OOM调优实例

共 6253字,需浏览 13分钟

 ·

2021-03-28 10:11

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

1.前言

之前预约小程序后台在当用户访问量增大时,tomcat老是宕机,在未发现原因时候需要重启。遂分析原因,在公司内部做了OOM调优实例分享,这里总结记录一下~

主要是从内存模型,线程池,以及dump文件方面入手~


本文涉及到的概念性东西请看

jvm垃圾收集算法以及垃圾收集器简介


多线程下出现 File has been moved - cannot be read again 请看

记一次印象深刻的bug—并发下File has been moved - cannot be read again


2.jvm 优化(ParNew+CMS)

硬件配置:生产上服务器 是 4核8g,四台服务器做负载。、


优化思路 尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。


2.1 内存模型分析

全国每天都要至少20w辆集装箱车进行运输行程预约。平时白天,因司机都在休息,预约数不多,基本没有预约。

夜间是大货车通勤高峰,特别是凌晨。我们假设有20w辆的集装箱车在5min内进行了预约,即每秒有将近650多次预约。


我们假设每秒有650次预约,也就是每秒有650个 运输行程对象( 里面包括了 挂车,企业,优惠等等模块的 其他对象信息)空间生成。负载均衡至4台服务器,每台大概160次。

我们可以估算一下对象大小,比如 int类型占用4字节,double类型占用8字节。也可以用 jol-core 直接算,大概1kb左右。


因为服务器是 4核8g,就可以给JVM进程分配四五个G的内存空间,那么堆内存可以分到三四个G左右,于是可以给新生代至少分配2G。


2.2 1.0 版本

‐Xms3072M ‐Xmx3072M ‐Xmn1536M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRatio=8

(1)-Xms 为jvm启动时分配 堆 的内存,比如-Xms3072M,表示分配3g

(2)-Xmx 为jvm运行过程中分配的 堆 的最大内存,比如-Xms3072M,表示jvm进程最多只能够占用3g内存

(3)-Xmn 年轻代 大小 1536M 代表1.5g

(4)-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

(5)‐XX:PermSize 非堆区初始内存分配大小(方法区 1.7,1.8时候叫元空间 用-XX:MetaspaceSize替代)

6)‐XX:MaxPermSize 最大的非堆区初始内存分配大小(1.7,1.8的 用‐XX:MaxMetaspaceSize 来替代)

(7)‐XX:SurvivorRatio 设置两个survivor和eden的比 8表示 survivor:eden=2:8

分析

系统按每秒生成60MB的速度来生成对象,大概运行20秒就会撑满eden区,会触发 minor gc, 大概会有95%以上对象成为垃圾被回收,可能 最后一两秒生成的对象还被引用着(流程还没执行完,对象还被引用着),暂估为100MB左右,那么这100M 会被挪到S0区,根据 动态对象年龄判断原则,这100MB对象同龄而且总和大于S0区的50%,那么这些对象都会被挪到老年代,到了老年代不到一秒又变成了垃圾对象,很明显,survivor区大小设置有点小,分析下微信小程序的业务知道,明显大部分对象都是短生存周期的,根本不应该频繁进入老年代,也没必要给老年代维持过大的内存空间,得让对象尽量留在新生代里。遂修改为2.0版本。


2.3 2.0 版本

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRatio=8

(1)-Xms 为jvm启动时分配 堆 的内存,比如-Xms3072M,表示分配3g

(2)-Xmx 为jvm运行过程中分配的 堆 的最大内存,比如-Xms3072M,表示jvm进程最多只能够占用3g内存

(3)-Xmn 年轻代 大小 1536M 代表1.5g

(4)-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

(5)‐XX:PermSize 非堆区初始内存分配大小(方法区 1.7,1.8时候叫元空间 用-XX:MetaspaceSize替代)

(6)‐XX:MaxPermSize 最大的非堆区初始内存分配大小(1.7,1.8的 用‐XX:MaxMetaspaceSize 来替代)

(7)‐XX:SurvivorRatio 设置两个survivor和eden的比 8表示 survivor:eden=2:8

将 年轻代的 大小 设置为2G

分析:

这样就降低了因为 对象动态年龄判断原则 导致的对象频繁进入老年代的问题。

对于对象年龄应该为多少才移动到老年代比较合适,微信小程序中一次minor gc要间隔二三十秒,大多数对象一般在几秒内就会变为垃圾,完全可以将默认的15岁改小一点,比如改为5,那么意味着对象要经过次5次minor gc才会进入老年代,整个时间也有一两分钟了,如果对象这么长时间都没被回收,完全可以认为这些对象是会存活的比较长的对象,可以移动到老年代,而不是继续一直占用survivor区空间。

对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统看下,有没有什么大对象生成(因为不是管理系统,没有导出excel等操作),预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过 1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的对象。


对于JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,比如我这里4核8G),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)


遂修改为3.0版本。


2.4 3.0 版本

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRatio=8

‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC

将 对象晋升到老年代年龄设置为5,大对象设置为1m,启用parNew收集年轻代(复制算法),CMS收集老年代(标记-清除 算法)

(1)-Xms 为jvm启动时分配 堆 的内存,比如-Xms3072M,表示分配3g

(2)-Xmx 为jvm运行过程中分配的 堆 的最大内存,比如-Xms3072M,表示jvm进程最多只能够占用3g内存

(3)-Xmn 年轻代 大小 1536M 代表1.5g

(4)-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

(5)‐XX:PermSize 非堆区初始内存分配大小(方法区 1.7,1.8时候叫元空间 用-XX:MetaspaceSize替代)

(6)‐XX:MaxPermSize 最大的非堆区初始内存分配大小(1.7,1.8的 用‐XX:MaxMetaspaceSize 来替代)

(7)‐XX:SurvivorRatio 设置两个survivor和eden的比 8表示 survivor:eden=2:8

(8)‐XX:MaxTenuringThreshold 对象晋升到老年代的阀值 默认是15

(9)‐XX:PretenureSizeThreshold 设置大对象的大小如果对象超过设置大小,会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew 两个收集器下有效。

(10)‐XX:+UseParNewGC 使用parNew 垃圾收集器 收集 年轻代,使用复制算法

(11)‐XX:+UseConcMarkSweepGC 使用cms收集器 收集 老年代 ,使用 标记-清除 算法

分析:

在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择 复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象 存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选 择“标记-清除”或“标记-整理”算法进行垃圾收集。注,“标记-清 除”或“标记-整理”算法会比复制算法慢10倍以上。遂修改为4.0版本。


2.5 4.0 版本

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:PermSize=256M ‐XX:MaxPermSize=256M ‐XX:SurvivorRatio=8

‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC

‐XX:CMSInitiatingOccupancyFaction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0


(1)‐XX:CMSInitiatingOccupancyFaction 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

(2)‐XX:+UseCMSCompactAtFullCollection FullGC之后做压缩整理(减少碎片)

(3)‐XX:CMSFullGCsBeforeCompaction=0 多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次

分析:

对于老年代CMS的参数如何设置,有哪些对象可能长期存活躲过5次以上minor gc最终进入老年代。

无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其量也就几十MB。还有就是某次minor gc完了之后还有超过200M的对象存活,那么就会直接进入老年代,比如突然某一秒瞬间要处理五六百次行程(比如有时候搞活动,不同时刻的不同预约可以走收费站优惠),那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧增,一次预约行程要好几秒才能处理完,下一秒可能又有很多预约行程过来。(即最后几秒产生的行程没有被minor gc掉,还存活者)

估算下大概每隔五六分钟出现一次这样的情况(这个要看政策,虽然不至于,但是也要考虑到,即 每五六 分钟会有一二百M对象挪到老年代),那么大概半小时到一小时之间就可能因为老年代满了(老年代此时是1G)触发一次Full GC,Full GC的触发条件,根据 老年代空间分配担保机制,历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了预约的最高峰期,后续可能几小时才做一次FullGC。对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理。


综上:只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值 选择4.0 最终版

如何选择垃圾收集器

(1)优先调整堆的大小让服务器自己来选择

(2)如果内存小于100M,使用串行收集器

(3)如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

(4)如果允许停顿时间超过1秒,选择并行或者JVM自己选

(5)如果响应时间最重要,并且不能超过1秒,使用并发收集器

(6)4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC


3.多线程下压缩图片异常

因代码中用了spring的线程线程池,将核心线程数调制cpu核数 ,拒绝策略调整为CallerRunsPolicy(满了之后,由当前线程执行)。


这里有个插曲,之前用了线程池,微信上传图片随机成功失败,有时出现异常:

File has been moved - cannot be read again

有兴趣的小伙伴可以关注我的博客,里面详细记录了这次bug


记一次印象深刻的bug—并发下File has been moved - cannot be read again源码分析


4.分析dump文件

生产挂了第一时间生成dump文件(当然因为内存太大也有可能不能生成),用jvisualvm进行分析

#看进程
jps 
# 使用hprof二进制形式,输出jvm的heap内容到文件。
# live子选项是可选的,假如指定live选项,那么只输出活的对象到文件。
jmap -dump:[live,] format=b,file=<filename> <pid> 
例如: 
      jmap -dump:format=b,file=jzx.hprof 17921 #堆快照


发现原因是 连接数太多而没有关闭,遂分析代码(连蒙带猜),发现其实代码中只有阿里云OSS一个地方创建连接较多(主要是其他都是用的spring框架默认的,只有阿里云OSS代码是自己封装的),遂分析发现,OSS所有方法创建连接后都关闭了,只有查看图片的方法没关(可能是当时查看官网API不仔细,给自己找个理由)



5.教训总结

根据实际并发量,估算好jvm模型,调整大小。

管理好线程池,在不熟悉的情况下用多线程一定要向技术好的人多请教请教。

且无论什么连接,或者流,都要关闭。

最别不要分析dump,因为每分析一次,都说明生产发生了一次重大bug(主要是测试没环境压测,其实就是测试太菜233333333)。

————————————————

版权声明:本文为CSDN博主「暴裂无球」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/weixin_42437633/article/details/107074499





锋哥最新SpringCloud分布式电商秒杀课程发布

👇👇👇

👆长按上方微信二维码 2 秒





感谢点赞支持下哈 

浏览 44
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报