假笨说-关于内存溢出,咱再聊点有意思的?
是虽然这个线程栈里看到了OOM,但是内存泄露其实不一定是和这个线程有关的,可能只是临门一脚而已
他打印一个Map,但是这个Map其实是一个ConcurrentHashMap,是线程安全的,但是这个map里的value是一个HashSet,这个HashSet是非线程安全的,并且存在多个线程修改这个Set的情况,那会不会是因为并发导致的呢,HashSet里其实就是一个HasMap的结构,我觉得是很有可能的,于是要同事自己去模拟下这个场景,看能否重现出来
http://blog.csdn.net/cctt_1/article/details/50589867
哦哦哦~排名第一的是byte[] , 我X. 其实根本查不出来是什么原因。除非你的代码里,很多 new byte[]. 否则使用byte[] 基本是你的调用的各种组件里的byte[]. 好吧。现在的怀疑的对象扩展到了自己的全部组件。。
不过这个参数只会生效一次,不会每次OOM的时候都做内存dump,大家可以想像一下,如果是代码的问题会发生连续的OOM,那连续做dump也没必要,于是JVM里控制这个参数只会在第一次发生OOM的时候做一次内存dump
他打印一个Map,但是这个Map其实是一个ConcurrentHashMap,是线程安全的,但是这个map里的value是一个HashSet,这个HashSet是非线程安全的,并且存在多个线程修改这个Set的情况,那会不会是因为并发导致的呢,HashSet里其实就是一个HasMap的结构,我觉得是很有可能的,于是要同事自己去模拟下这个场景,看能否重现出来
注意,这个得在JDK6或者7下跑才会重现,JDK8下不存在这个问题
Demo里就是两个线程同时对HashSet进行修改,可能带来的一个后果是里面的HashMap因为要扩容并且做rehash而出现死循环的情况,当有线程要打印这个HashSet的时候,会调用其toString方法,再看看其父类AbstractCollection的toString的逻辑:
就是挨个遍历,然后将值塞到StringBuilder里,如果正巧之前因为多线程的并发操作导致了死循环链的产生,那可能会导致这个StringBuilder会非常大,并且还会不断进行扩容,正如上面的堆栈看到的一样,这直接带来的一个后果就是出现内存溢出
对于同事线上碰到的那个问题看到的OOM提示是
Requested array size exceeds VM limit
,这个提示讲真我还是第一次碰到有发生的,假如说你的内存其实非常大,足够的剩余,但是当你要创建一个数组的时候,如果你的数组的长度超过Integer.MAX_VALUE-2
的话,那你将会看到一个这个提示的OOM抛出来,其实这也是你能创建的数组的最大长度了,这或许很多人都没有注意到的,就把这个当做本文的一个最有价值的亮点吧
发现内存泄露除了仔细看代码的确没有太好的方法。首先看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()去掉就立马好了。