充满BUG的世界观——再遇Java内存泄露

发现内存泄露除了仔细看代码的确没有太好的方法。首先看gc log, 确定是内存泄露,而不是内存不够。内存泄露的特点就是以每次Full
GC后使用的最低内存为起点,拟合一条线。如果这条线是随时间递增的一条曲线,那么很大程度上代表着内存泄露。

然后使用 jmap -histo [pid] 来查看你的所有对象所占内存的比例。你可能很不幸的发现[B 这个byte数组对象占用了绝大多数。这的确没有更好的
方法了。只能一点点的看代码。检查一下有没有写成循环的地方。检查一下有没有申请的内存没有释放。检查一下全局变量或者单例中的map啥的。最后,你大概只能以怀疑一
切的态度检查所有的代码。

ok. 下面以八卦的方式讲讲我这次遇到的内存泄露(memory leak).

这几天一直在写一个入库组件。这个组件的目的就是解析传输过来的数据,并写入到数据库中。嗯。听起来很简单。但是解析格式比较复杂,而且还要使用某种特定的计算公式进
行去重处理。另外解析的数据需要以上千条记录的形式输入到数据库中。嗯,为什么会这样设计呢?因为历史遗留问题。。。嗯历史遗留问题这几个字非常管用。不管放在什么语
境中,反正觉得困难就可以说历史遗留问题。其实是为了小步快跑,慢慢申请时间。上头大概不会有人希望你这模块做大半年还没有做来。希望你这么做的,大概都是你的死敌。

好吧上面就历史背景。在这个背景下。我这边入库组件,写了一个分布式的,多线程解析,多线程批量入库的代码。本来觉得这种设计挺好。但是因为gap lock,
多线程和分布式的问题,批量写总是在不知不觉中造成死锁。为了代码的精简,为了不引入更多的问题,索性将批量写改为了单条写。

好了,这下memory leak问题来了。难道是因为之前都死锁了,所以memory leak 就没有暴露出来? 有可能哦~

先看一下gc log。为啥是memory leak。看一下下图。其中蓝线的是内存使用情况。蓝线的最低点是每次Full GC
带来的内存大量释放。GC的最低点你可以看到是不断增加的。所以很大的可能是内存泄露。

![](http://img.blog.csdn.net/20160126214648953?watermark/2/text/aHR0cDovL2Jsb2
cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravit
y/Center)
为什么说很大可能是内存泄露呢?不同的程序有不同的内存使用模式。比如说我的程序中有一个大map.
这个map会不断的填充数据。但是数据集是有限的。但是这个map在最终填充完毕前,内存的使用量会不断的增长。如果内存不够了,仍然会有outofmemory 错
误,并且那个gc的图和我给的gc图会很像。这不代表这内存泄露,这只能说内存不够用。但是这需要先证明数据集是有限的,并且系统空闲内存可以完全放入这个数据集。否
则你只能采取其他方法来防范内存不够用的情况。

好吧。我看到这个图。感觉是内存泄露。为啥?因为我的数据集有限,并且粗略算了一下也不大。即使有缓存计算的策略存在,这些空间仍然不会造成outofMemory的
现象。那到底是什么原因?先使用一下jmap -histo [pid]. 看了一下比例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

num #instances #bytes class name
----------------------------------------------
1: 21571308 1163654064 [B
2: 1770275 125384008 [I
3: 1715985 120562976 [[B
4: 1715382 120535928 [Ljava.io.InputStream;
5: 3430930 109789592 [Z
6: 1715198 68607920 com.mysql.jdbc.PreparedStatement$BatchParams
7: 621372 44778960 [C
8: 59015 11608344 [Ljava.lang.Object;
9: 469551 11269224 java.lang.String
10: 335730 8057520 org.dom4j.tree.DefaultAttribute
11: 76733 2455456 org.dom4j.tree.DefaultElement
12: 49621 2376880 [Ljava.lang.String;
13: 47685 1525920 java.util.HashMap$Node
14: 41482 1327424 com.paratera.importdata.CacheKeys
15: 46753 1122072 java.lang.StringBuilder
16: 44715 1073160 java.util.ArrayList
17: 32577 1042464 java.util.concurrent.ConcurrentHashMap$Node
18: 40990 983760 java.lang.Long
19: 13911 667728 java.nio.HeapCharBuffer
20: 13832 663936 java.nio.HeapByteBuffer
21: 24729 593496 java.lang.StringBuffer
22: 13476 539040 [Ljava.util.Formatter$Flags;

哦哦哦~排名第一的是byte[] , 我X. 其实根本查不出来是什么原因。除非你的代码里,很多 new byte[]. 否则使用byte[]
基本是你的调用的各种组件里的byte[]. 好吧。现在的怀疑的对象扩展到了自己的全部组件。。

前五名都看不出任何问题。直到第六名。

![](http://img.blog.csdn.net/20160127111800214?watermark/2/text/aHR0cDovL2Jsb2
cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravit
y/Center)
就是这货。这货的变量里 byte[] byte[][] , int[] , InputStream[]
排名前几的都有!!!不用说了。一定是这货。这货就是mysql jdbc执行 addBatch() 放入的。然后我细细看了一下代码。发现
executeBatch() 调用才会清理了 addBatch()中的 BatchParam,
如果只是调用了execute()方法,则写入数据库时不会清理BatchParam.
所以还是之前将批量入库转换为单条数据入库导致的。只将executeBatch()改为了execute(),
虽然功能上立马变成了单条数据入库。但实际上却直接引入了内存泄露问题。

当然因为代码封装/函数化的问题, addBatch() 和 executeBatch()
被放入到了不同的函数中。。。。所以再次自己挖坑自己埋。自己掉坑里,一定是自己之前坑挖的不对。最后的解决方法也很简单。将addBatch()去掉就立马好了。

好了。。。。写的多了。。。天晚了。。。洗洗睡去了。。。

** 哦对了。打条公司信息,有想加入【并行科技】java团队的发简历到hr@paratera.com(请注明来源,方便过hr关).  公司只做高大上的超算/高性能计算的 toB  toG 业务,不做 toC业务。待遇不是问题。问题是你到底值不值你要的待遇。。。 **