发现内存泄露除了仔细看代码的确没有太好的方法。首先看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的最低点你可以看到是不断增加的。所以很大的可能是内存泄露。
为什么说很大可能是内存泄露呢?不同的程序有不同的内存使用模式。比如说我的程序中有一个大map. 这个map会不断的填充数据。但是数据集是有限的。但是这个map在最终填充完毕前,内存的使用量会不断的增长。如果内存不够了,仍然会有outofmemory 错误,并且那个gc的图和我给的gc图会很像。这不代表这内存泄露,这只能说内存不够用。但是这需要先证明数据集是有限的,并且系统空闲内存可以完全放入这个数据集。否则你只能采取其他方法来防范内存不够用的情况。
好吧。我看到这个图。感觉是内存泄露。为啥?因为我的数据集有限,并且粗略算了一下也不大。即使有缓存计算的策略存在,这些空间仍然不会造成outofMemory的现象。那到底是什么原因?先使用一下jmap -histo [pid]. 看了一下比例。
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[]. 好吧。现在的怀疑的对象扩展到了自己的全部组件。。
前五名都看不出任何问题。直到第六名。
就是这货。这货的变量里 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业务。待遇不是问题。问题是你到底值不值你要的待遇。。。