Abstract
Duration: 2021-03-21
Description:
马士兵 JVM训练营中再现的线上内存泄漏的demo , 模拟一个评估用户是否具备放贷资格的模型。每次去数据库取出所有用户,然后使用延迟线程每3秒对所有用户做资质评估。
Scope of influence:
造成内存泄漏,full gc频繁,内存越来越来,每次fullgc回收的内存越来越少。最终因无可回收垃圾而内存又益处,程序自杀。
示例代码
public class OOM2UserCredit {
private static class CardInfo{
BigDecimal price = new BigDecimal(10);
String name ="张三";
int age=5;
Date birthDay = new Date();
public void m(){}
}
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) throws InterruptedException {
executor.setMaximumPoolSize(50);
while (true){
// 是否符合 放贷资格
modelFit();
Thread.sleep(100);
}
}
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info-> {
executor.scheduleWithFixedDelay(()->{
info.m();
},2,3, TimeUnit.SECONDS);
});
}
//数据库拉取所有 信用卡用户信息
private static List<CardInfo> getAllCardInfo() {
List<CardInfo> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo info = new CardInfo();
list.add(info);
}
return list;
}
}
Failure causes
- 查看gc日志,发现full gc越来越频繁
- jvm大盘查看内存越来越少 每分钟gc次数线性增长 怀疑是内存泄漏
- jmap 堆导出 查看最占内存的类
注意:当线上堆内存在1G以下,可以直接导出并重启(暂时解决内存泄漏) 如果堆内存大于2G,经实测试 MacPro i5 10多分钟没打开,i9处理器的win电脑不到1分钟打开,因此 当dump文件大于2G,应当采取其他方式,比如说运维直接操作线上机器 执行jmap ? ? | head 100 或者在预发环境模拟。
定位到FutureTask和CarInfo 这两个类的实例对象最多 占用内存最多 且只增不减少(即使fgc时也不见减少)
根据CarInfo定位到相关使用多线程部分的类,也就是示例中的OOM2UserCredit.main方法
分析目标类OOM2UserCredit
如上代码块,modelFit()这个方法是在循环体中,也就是说每0.1秒就会new一个CardInfo和FutureTask加入线程池任务队列等待执行,而任务是每3秒执行一次的,这就造成了任务堆积。因为线程拒绝策略是放弃旧任务,那么一段时间过后,线程中的线程任务数将始终维持在最大值(延迟线程池默认DelayedWorkQueue的最大任务数是Integer.MAX), 也就是说该进程会持续创建Integer.max个FutureTask和CardInfo等待 线程池执行。这么多对象被线程池的队列引用着,full gc也无法回收,于是就看到了前面的现象,gc越来越频繁,回收效果越来越差,直到OOM
Resolve procedure
- 修改代码
如上定位到可能的原因之后,立即修改代码.
没找到马老师的官方解答
- 测试验证 tcpcopy 导流到测试环境
arthas + Jprofiler remote server 查看CardInfo类full gc之后 不应该实例个数骤减
- 上线观察一段时间 内存稳定