Tuesday, January 26, 2016

Java OutOfMemoryError - trouble shooting



假笨说-关于内存溢出,咱再聊点有意思的?
不过这个参数只会生效一次,不会每次OOM的时候都做内存dump,大家可以想像一下,如果是代码的问题会发生连续的OOM,那连续做dump也没必要,于是JVM里控制这个参数只会在第一次发生OOM的时候做一次内存dump

是虽然这个线程栈里看到了OOM,但是内存泄露其实不一定是和这个线程有关的,可能只是临门一脚而已

他打印一个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抛出来,其实这也是你能创建的数组的最大长度了,这或许很多人都没有注意到的,就把这个当做本文的一个最有价值的亮点吧

http://blog.csdn.net/cctt_1/article/details/50589867
发现内存泄露除了仔细看代码的确没有太好的方法。首先看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()去掉就立马好了。

Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts