一次log4j的导致的OOM 破案之旅

6,251 阅读2分钟

起因

上周下午,正当我准备早早下班的时候,收到了一条报警信息,XX机器的XX服务不可用,按照运维操作步骤,先把服务恢复再说,由于服务是高可用,对业务没有影响,但是还是要找出出现问题的原因,避免下次再出现。按照之前的文档处理完重启之后,需要花点时间定位一下为什么进程会退出。

日志

首先,关注是日志,在日志中,出现了下面的信息

java.lang.OutOfMemoryError: GC overhaed limit exceeded . 

一般来说,导致OOM的有两种情况:

  • 数据量增加,导致的堆的空间不够
  • 内存泄漏,对象没有及时回收。

GC overhaed limit exceeded 一般出现的原因就是程序把JVM的内存都耗尽了,GC花费了太多的时间来回收内存,当JVM耗费了90%以上的时间却回收不回来2%的空间的时候,就会抛出这个异常。

从这里就可以知道,我们的程序里面存在没有办法回收的内存,那么是什么?

HeapDump 文件

我们的JVM程序在启动的时候,通过以下参数设置了如果出现OOM的时候就把异常dump出来。

-XX:HeapDumpOnOutOfMemeryError -XX:HeapDumpPath=$path/jvm.hprof 

通过分析hprof文件就可以清楚看到存在那些内存。

jhat

一开始,我使用了jhat文件,从上面的异常我初步判断可能跟日志打印有关系,开启jhat ,

通过Heap Histogram, 可以看到几个占用内存毕竟打的类都和log4j有关,查看log4j里面的Logger的引用,发现有个Hashtable保存了Logger的引用。

log4J

既然跟log4j有关系,那就好办了,只需要看一下程序是怎么用的log4j和log4j是管理logger情况就知道了。 下面代码是就是程序使用logger,和其他使用logger没有什么异常,唯一不同的时候,因为我们是分布式调度程序,每一个任务的实例被调用的时候会传入不同的logName。

logger = Logger.getLogger(logName);

如果没有仔细去看Logger.getLogger里面的实现的话,这样的情况使用起来是理所当然,但是当看到getLogger里面的实现后,就会知道我们使用这种方式存在的缺陷。

Logger.getLogger 调用了 LoggerManager.getLogger , 实现如下

 public static Logger getLogger(String name) {
        return getLoggerRepository().getLogger(name);
    }

LoggerRepository 有两个实现,分别是:

  • Hierarchy
  • NOPLoggerRepository

默认的实现方式是,Hierarchy

Hierarchy 会通过一个ht = new Hashtable(); 保留所有的logName到logger的映射。而我们的程序是每一个调用一个任务的时候就会生成一个Logger,累计了那么久就产生了几十万个对象。

参考资料: 介绍JVM中OOM的8种类型