背景知识 (background)
老年代的对象大多是经过多轮Young GC后晋升上来的,即对象在堆里存活的时间比较长。
老年代内存不足通常意味着内存泄露,伴随着频繁的FullGC,可能会有较大停顿,甚至停顿十几秒,导致健康检查失败或接口超时。
目前老年代内存占用超过80%且持续2分钟,则会触发报警。
查看指标 (dashboard)
其中,Old Gen(heap)
即老年代内存占用 ,min 即所选时段最小值,max 为所选时段最大占用,current为当前内存。
另外,开发也应关注监控面板:GC耗时 和 GC原因与频次。
止损措施 (action)
若想保存案发现场,可临时摘流后dump内存。
若想快速恢复,去私有云/容器平台手动重启报警节点
,一般重启会平滑摘流、优雅停机。
事后改进(postmortem)
老年代内存不足,大多是内存泄露,建议摘流后Dump堆内存,联系OP下载到本地MAT内存分析,及时修复。
我们整理了一些常见的原因(cause),仅供参考。
可能的原因 (cause)
内存泄露的原因很多,大多是对象的生命周期太久了。
一、对象被长生命周期的组件引用
常见的全局静态变量、单例对象等强引用临时变量。
比如,Map每次调用Put添加对象,但缺乏淘汰机制;本地缓存没有限制最大缓存数量;ThreadLocal没有正确的清理数据;一次性加载过多数据到内存里;
在性能统计等场景使用Map时,一定要确保在任意编码习惯下的 Key / Value内存开销是可控的,比如,Druid 数据源的SQL统计,我们之前遇到过有的项目SQL语句几百KB,导致SQL统计占用过多内存。
有的项目使用OpenFeign + PathVariable/RequestParam 请求Restful接口,但指标生成时,取了动态生成的uri导致指标数量膨胀。
有的项目使用MyBatis的foreach,传递了过多的参数,导致MyBatis生成的临时大对象存在过久,不能被及时回收。
有的组件使用JVM Runtime#addShutdownHook
注册钩子销毁资源,那这个对象会一直被JVM Hook引用,若创建了很多实例,则都不会被回收。
二、内存分配速率过大
可参考监控面板:GC内存分配,了解每秒钟新生代分配了多少内存、多少内存晋升到了老年代。
内存分配过大,可能是一次性加载的数据过多,通常伴随着慢请求、慢查询、不合理的报表导入导出或文件上传下载机制。
对G1来说,大对象(Humongous)会直接在老年代分配,其他GC算法也有类似机制,超过一定阈值直接晋升到老年代。
如果业务逻辑执行过久,则会导致对象不能被及时回收。
建议通过应用大盘的常用排行榜 ( HTTP慢接口/慢SQL排行 ) 或查看当时的Accesslog、应用日志来筛选可能的请求,尽可能优化代码实现,降低单次执行的资源消耗
。
另外,可尝试 hawk的HeartBeat 、arthas 查看当时的线程堆栈,找到可能的慢请求。