目录
1背景:
最近公司线上项目出现了java.lang.OutOfMemoryError ......java heap space.....记录总结一下。
1.1:什么是内存溢出,为什么会内存溢出?
首先,内存溢出简单的理解就是内存不够用了,这里的内存不足就是指堆的内存不够用了,附一张JVM内存模型
那为什么会不够用了呢?
原因:Java虚拟机内存模型中堆用于存储对象实例,当GC Roots到创建的对象之间有可达路径时,就可以避免垃圾回收机制回收这些对象,那么只要不断地创建这样的对象,当数量到堆的容量限制后就会产生堆内存溢出异常(OOM for Heap)
2排查问题:
-
等待阶段:由于出现错误时项目还没有接入监控,所以我们不能第一时间发现错误,无法第一时间用jmap -dump:format=b,file=heap.bin dump出内存快照文件,于是我们在启动参数上添加了-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/heap/dump参数来发生oom时能dump出内存快照文件,以便后续我们用MAT分析。
-
着手排查:从运维拿到快照文件之后我们就开始用MAT开始分析,下图是MAT分析完系统堆内存的使用情况:
Overview视图
该视图会首页总结出当前这个Heap dump占用了多大的内存,其中涉及的类有多少,对象有多少,类加载器,如果有没有回收的对象,会有一个连接,可以直接参看(图中的Unreachable Objects Histogram)。
比如该例子中显示了Heap dump占用了1.5GB的内存,24.7K个类,48.8m个对象,864个类加载器。
然后还会有各种分类信息
histogram视图(重要)
histogram视图主要是查看某个类的实例个数,比如我们在检查内存泄漏时候,要判断是否频繁创建了对象,就可以来看对象的个数来看。也可以通过排序看出占用内存大的对象:
Dominator Tree(重要)
此视图中列出了每个对象(Object Instance)与其引用关系的树状结构,同时包含了占用内存的大小和百分比。通过Dominator Tree视图可以很容易的找出占用内存最多的几个对象(根据Retained Heap或Percentage排序)Histogram视图和Dominator Tree视图的角度不同,前者是基于类的角度,后者是基于对象实例的角度,并且可以更方便的看出其引用关系。
这里我们发现有个类占用了大量的内存,并且根据线程id去系统中查询日志定位到了具体的方法,发现两者不谋而合,最后找到了问题,原因是:这个方法一次返回了几十万的数据赋值给list,并且没有缓存,当多个线程请求时就造成了oom,那到底是不是这个问题造成的呢?我们来复现一下。
3问题复现:
既然找到了问题那我们就要复现一下这个问题发生的情景,把代码部署到测试环境,数据确保返回的量级和生产的一样,这里我用jmeter来反复调用
我们用jmap -heap xxx(进程id)调用前堆内存使用情况:
图一是初始状态,图二是当我们用jmeter不断调接口后堆的使用情况,由于测试环境堆大小只有1G,所以Eden区和老年代很快就已经满了。图三当使用 jstat -gcutil 查看gc情况发现频繁的youngGC并没有使eden区的使用率下降,而是不断地晋升到老年代,最后fullGC同样没能将老年代的中的对象gc掉。再用jmap -histo 命令查看内存占用排名又发现了我们一开始的那个类。那么问题就确定了。可以着手解决了。
4解决问题:
问题已经确定了 ,是方法一次加载了几十万的数据造成的,由于数据量大,接口返回的速度较慢,用户可能等待过程中重复点击前端的按钮,这样一次发起了多个请求造成的。我们的解决方法就是前端做一下防止重复提交的限制,点击完未返回数据按钮不可点,后端可以用redis做一下缓存,考虑分批次加载数据;另外这个方法加载的数据需要一定的逻辑处理后的结果才是最终需要用到的,于是我们把这个方法的结果入库,将业务逻辑处理提前完成,避免实时处理。
最后附上出现内存溢出异常时常用的解决办法(转自Java面试那些事):
检查代码中是否有死循环或递归调用。
检查是否有循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询可能就会引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查List、Map等集合对象是否使用完未清除的问题,List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
检查对大文件的读取是否采用nio的方式。
5总结:
排查内存需要了解一些jvm的知识,这样你才能知道问题的来龙去脉,可以有正确的思路去解决它;另外的我们项目用的是ParallelGC,如果你的项目代码已经没有优化的地步了,可以考虑一下jdk1.8以后更流行的G1垃圾回收器。