起因
上周下午,正当我准备早早下班的时候,收到了一条报警信息,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种类型