曾经,有个项目时不时总是会报错:
java.lang.OutOfMemoryError: GC overhead limit exceeded
这个问题几个同事都没搞定,就来找我。我看了看,突然想起来,以前在官方调优指南《HotSpot Virtual Machine Garbage Collection Tuning Guide》看到过相关介绍。
JVM 本身内存不足就会运行 GC,但是如果每次 GC 回收的内存不够,那么很快就会开始下一次 GC。
JVM 有个默认的保护机制,如果发现在一个统计周期内,98% 的时间都是在运行 GC,内存回收却少于 2% 的时候,就会报这个错。
怎么引起的呢?这个问题如果去排查代码,那真的是难如登天,首先,没有任何堆栈错误去帮助定位问题。其次,项目代码量大了去了,而且是年头久远。
这时,就需要通过对 JVM 总体的深入理解,去反推问题了。我当时是这样推理的:
内存溢出,GC 无法回收问题,说明了两个问题:
- 堆内的内存不够用了
- 占用内存的对象要么就是该关闭的资源没有关闭,要么被大量的暂时放在一起了
那如果我 dump 出内存文件出来,再分析下就知道是哪些对象在占用内存了。
一查发现是大量的字符串在占用内存。
综合我前面的推测,字符串不是数据库连接,肯定没有该关闭未关闭的问题。那就剩一个可能了,就是被大量的暂时放起来了,导致 GC 回收不了。
那么新问题来了,能大量放字符串的,会是什么?
首先就去猜缓存。根据这条线索,直接去源码搜 Cache 关键词,把所有关于 Cache 的代码都看了下。一下子就找到问题了。
原来,我们有个功能是解析一个非常大的文件。文件的格式如下:
需要把这个文件的每一行内容按照列去一起存到数据库里。
由于写代码的人偷懒,想一次解析完毕后一股脑全塞到数据库里。所以,他弄了个 Map,Map 的 Key 是相同的列名,Value是每一行解析过的内容。
而这样写代码的结果就是,一行对应了一个有三个条目的 HashMap。如果文件有十几万行,就有十几万的 HashMap。然后,这些 HashMap 再存到一个列表里,再把这个列表放到一个叫做 xxxCache 的 HashMap 中。
示意代码如下:
public class ParseFile4OOM {
public static void main(String[] args) {
List<Map<String, String>> lst = new ArrayList&