关于java:错误使用线程池导致OOM

景象形容

在线上运行了几个月的服务忽然爆出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 RootStack LocalClassJNI LocalJNI GlobalLive 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,或者直到过程退出时子线程才会退出

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理