百万级java后台生产OOM调优实例
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
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 秒
感谢点赞支持下哈