充满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的最低点你可以看到是不断增加的。所以很大的可能是内存泄露。


充满BUG的世界观——再遇Java内存泄露_第1张图片


为什么说很大可能是内存泄露呢?不同的程序有不同的内存使用模式。比如说我的程序中有一个大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[]. 好吧。现在的怀疑的对象扩展到了自己的全部组件。。


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

充满BUG的世界观——再遇Java内存泄露_第2张图片


就是这货。这货的变量里 byte[] byte[][] , int[] , InputStream[] 排名前几的都有!!!不用说了。一定是这货。这货就是mysql jdbc执行 addBatch() 放入的。然后我细细看了一下代码。发现 executeBatch() 调用才会清理了 addBatch()中的 BatchParam, 如果只是调用了execute()方法,则写入数据库时不会清理BatchParam. 所以还是之前将批量入库转换为单条数据入库导致的。只将executeBatch()改为了execute(), 虽然功能上立马变成了单条数据入库。但实际上却直接引入了内存泄露问题。


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


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


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


你可能感兴趣的:(充满BUG的世界观——再遇Java内存泄露)