共计 1494 个字符,预计需要花费 4 分钟才能阅读完成。
景象形容
在线上运行了几个月的服务忽然爆出 OOM,重启之后隔几天又再次爆出 OOM。就把内存从 512M 加大到了 2G,惊涛骇浪一周后再次爆出 OOM,这就只能是代码问题了!!!
排查过程
因为线上没有 jvm dump
文件,就只能人肉排查代码了。
嫌疑一:全量查问
select * from table
这种 SQL,当数据量够大的是否就会导致 OOM。
然而这个我的项目中没有这样的查问,只能持续排查。
嫌疑二:错误操作汇合
出于业务目标,在筛选时须要把专用的和专用的 handler
都放入备选汇合,于是就呈现了下列错误代码。
public class Manager{private List<Handler> commonHandlers = new ArrayList<>();
private Map<Type, List<Handler>> handlers = new Hashmap<>();
public List<Handler> dispatch(Condition condition) {List<Handler> alternativeHandlers = handlers.get(condition.getType());
alternativeHandlers.addAll(CommonHandlers); // 专用的处理器也须要退出备选
// 其余筛选操作
handlers = alternativeHandlers.stream()
.filter(.......)
.....
return handlers;
}
}
每一次调用 dispatch
办法都会批改 handlers
中的 List
导致数据越来越多,因为数据量较小在短期内先爆出了业务逻辑 BUG,同时这类 BUG 实践上也会导致 OOM。然而排查发现这个我的项目中没有这样的操作,只能持续排查。
嫌疑三:线程池
Java8 HotSpot JVM 的 GC 是通过可达性剖析来断定垃圾的,而这个可达性的出发点就是 GC Root
有 Stack Local
、Class
、JNI Local
、JNI Global
、Live Thread
,其中前 2 个曾经被前 2 步排除了嫌疑,而本我的项目又不波及到 JNI
相干操作,那么就剩下 Live Thread
一种可能了。
依据前 2 步可知,线程所援用的对象都没有 OOM 的可能,那么就只可能是线程自身始终在减少,最终导致 OOM。依照这个思路发现了上面的嫌疑代码
public class Handler{private ThreadPoolExecutor executor = new ThreadPoolExecutor(.....);
public void handle(Object arg) {executor.submit(()->{
// 理论解决的业务逻辑
.........
});
}
}
这个代码的问题在于,Handler
自身会被 Manager
定时刷新,每次刷新时会构建一个新的 Handler
并将旧的抛弃。然而旧的 Handler
中线程池并没有被敞开,线程池中的线程也就会始终存活,随着工夫的减少,废除的线程越来越多最终导致了 OOM。
锁定罪犯 : 须要更新的对象所援用的线程池,在对象创立新实例时,旧实例的线程池没有被敞开,而是被间接摈弃了。
解决方案 : 抛弃旧的Handler
之前手动shutdown()
关联问题?
单例对象援用的线程池或者线程,主线程退出时没有敞开为什么没有造成 OOM?
一个 Java Application 独占一个 JVM 过程,退出主线程时会退出过程,过程中的所有线程也将退出。退出了也就开释了内存了当然也不会有内存问题了。
没有 shutdown 的线程会怎么?
会持续运行,直到被 shutdown,或者直到过程退出时子线程才会退出