背景
服务的其中一个实例健康检查一直频繁上下线告警,已使用的堆内存几乎达到分配的最大值,运维导出堆栈、内存快照重启服务,接下来就是复盘。
原因
功能场景是分层推送信息。循环调用户中心服务接口分批查询指定用户群的用户,然后给这些用户推送消息。其中查询回来的用户id会在内存中用HashSet做去重操作,当时有两个任务同时在进行,两个HashSet装了两千多万个用户id,内存占用超过了1.9G,一直频繁GC,影响了业务线程。当时的jvm监控如下:
图1
图2
图3
图表分析
- 图 1可以看到堆内存从上午8点开始一直锯齿型增长,因为那两个任务还没完成,HashSet内存一直释放不了,
- 从图2看到,一直到上午11点老年代已经满了,之后对象晋升老年代失败就会full gc了
- 从图3左上角看到,年轻代占用越来越多且无法回收
- 从图3右下角看到,11点后每次GC的耗时达到了1min,这时候基本无法正常提供服务了
处理方案
1、临时注释去重操作的代码发版,后续再优化(布隆过滤器优化内存占用过大问题)
2、反馈相关业务方使用不当问题。这次是业务方使用不当造成的,正常目标用户体量不到两百万,但是全量一千多万地发送了,还同时发两个。
总结与思考
1、程序用内存做缓存时,还是不能太乐观,要考虑极端情况导致内存溢出(一开始特意配了2G最大堆内存, T_T 没想到还碰上OOM)。
2、目前只有服务器总体内存使用告警,jvm内存异常却没告警,需要加上,以便及早发现问题
3、系统如果增加预览结果展示并提醒再次确认,或增加审批机制等提醒用户反复检查,是否可以避免这次的问题呢
补充说明
线上服务jvm参数
-Xms1024m
-Xmx2048m
-XX:-OmitStackTraceInFastThrow 大量抛出同样的异常的后,后面的异常输出将不打印堆栈
-XX:+UseParNewGC 新生代进行并行回收
-XX:AutoBoxCacheMax=20000 缓存自动装箱最大值
-XX:-UseBiasedLocking 禁用偏向锁
-XX:+CMSParallelRemarkEnabled 启用并行重标记,降低标记停顿
-XX:+UseConcMarkSweepGC 并发标记清除(CMS)收集器
-XX:CMSInitiatingOccupancyFraction=50 CMS对内存超过50%时开始GC
-XX:+CMSScavengeBeforeRemark 在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销,一般CMS的GC耗时 80%都在remark阶段
-XX:MaxTenuringThreshold=15 设置的是年龄阈值
-XX:+UseCMSInitiatingOccupancyOnly 只用设定的回收阈值(上面指定的50%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.
-XX:CMSFullGCsBeforeCompaction=1 1次Full GC后对内存空间进行压缩整理
-XX:+UseCMSCompactAtFullCollection Full GC后对内存空间进行压缩整理
-XX:+DisableExplicitGC 手动调用System.gc()不生效
CMS垃圾收集器处理过程有七个步骤:
1. 初始标记(CMS-initial-mark) ,会导致STW;
2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
5. 重新标记(CMS-remark) ,会导致STW;
6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;