错误使用线程池导致OOM
现象描述
在线上运行了几个月的服务突然爆出OOM,重启之后隔几天又再次爆出OOM。就把内存从512M加大到了2G,风平浪静一周后再次爆出OOM,这就只能是代码问题了!!!
排查过程
由于线上没有jvm dump
文件,就只能人肉排查代码了。
嫌疑一:全量查询
select * from table
这种SQL,当数据量够大的是否就会导致OOM。
但是这个项目中没有这样的查询,只能继续排查。
嫌疑二:错误操作集合
出于业务目的,在筛选时需要把专用的和公用的handler
都放入备选集合,于是就出现了下列错误代码。
public class Manager{private List commonHandlers = new ArrayList<>();
private Map handlers = new Hashmap<>();
public List dispatch(Condition condition) {
List 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
一种可能了。【错误使用线程池导致OOM】根据前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,或者直到进程退出时子线程才会退出
推荐阅读
- 由浅入深理解AOP
- 【译】20个更有效地使用谷歌搜索的技巧
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus|MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决
- MybatisPlus使用queryWrapper如何实现复杂查询
- iOS中的Block
- Linux下面如何查看tomcat已经使用多少线程
- 唱歌教学(导致嗓音损坏的几个常见的错误唱歌方法!)
- 调取接口时报404错误(ID:16)
- 使用composer自动加载类文件