内存泄漏排查 MAT工具使用

时间:2024-03-28 09:40:56

説在前面:

尽管Java虚拟机可以帮我们对内存进行回收,但是其回收的是Java虚拟机不再引用的对象。

很多时候我们使用系统的IO流,Cursor,Receiver如果不及时释放,就会导致内存泄漏,这些场景是常见的,一般开发人员也都能够避免。

但是,很多时候内存泄漏的现象不是很明显,

1.比如内部类,Handler相关的使用导致的内存泄漏,

2.或者你使用了第三方library的一些引用,比较消耗资源,但又不是像系统资源那样会引起你足够的注意去手动释放它们。

当代码越来越多,如果结构不是很清晰,即使是常见的资源也有可能略掉,从而导致内存泄漏。

内存泄漏很有可能会导致内存溢出,就是常说的OOM,从而导致应用crash,给用户一种糟糕的体验。

最后通过内存泄漏分析,集合使用率,Hash性能分析,OQL快读定位空集合实战演示如何在实际应用中使用MAT。(通过一些静态检测也可以在开发期发现一些内存泄漏的问题,后面会有一些静态检测的文章)

造成OutOfMemoryError原因一般有2种:

1、内存泄露,对象已经死了,无法通过垃圾收集器进行自动回收,通过找出泄露的代码位置和原因,才好确定解决方案;

2、内存溢出,内存中的对象都还必须存活着,这说明Java堆分配空间不足,检查堆设置大小(-Xmx与-Xms),检查代码是否存在对象生命周期太长、持有状态时间过长的情况。

#一 相关概念 

Java虚拟机如何判定内存泄漏的呢?下面介绍一些相关概念


这里也要说明一下Java的引用规则:从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。
* Strong Ref(强引用):通常我们编写的代码都是Strong Ref,于此对应的是强可达性,只有去掉强可达,对象才被回收。
* Soft Ref(软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。一般可用来实现缓存,
           通过java.lang.ref.SoftReference类实现。
* Weak Ref(弱引用):比Soft Ref更弱,当发现不存在Strong Ref时,立刻回收对象而不必等到内存吃紧的时候。
           通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
* Phantom Ref(虚引用):根本不会在内存中保持任何对象,你只能使用Phantom Ref本身。
        一般用于在进入finalize()方法后进行特殊的清理过程,通过 java.lang.ref.PhantomReference实现。



##1.1 GC Root ##
JAVA虚拟机通过可达性(Reachability)来判断对象是否存活,基本思想:以”GC Roots”的对象作为起始点向下搜索,搜索形成的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达的),则该对象被判定为可以被回收的对象,反之不能被回收。

GC Roots可以是以下任意对象

  • 一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量)
  • 线程自身或者system class loader(系统类加载器)加载的类
  • native code(本地代码)保留的活动对象

##1.2 内存泄漏
对象无用了,但仍然可达(未释放),垃圾回收器无法回收。

##1.3 强(strong)、软(soft)、弱(weak)、虚(phantom)引用 ##

Strong references

普通的java引用,我们通常new的对象就是:
StringBuffer buffer = new StringBuffer();
如果一个对象通过一串强引用链可达,那么它就不会被垃圾回收。你肯定不希望自己正在使用的引用被垃圾回收器回收吧。但对于集合中的对象,应在不使用的时候移除掉,否则会占用更多的内存,导致内存泄漏。


Soft reference
当对象是Soft reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收它。因此Soft reference适合用于构建一些缓存系统,比如图片缓存。

WeakReference

WeakReference不会强制对象保存在内存中。它拥有比较短暂的生命周期,允许你使用垃圾回收器的能力去权衡一个对象的可达性。在垃圾回收器扫描它所管辖的内存区域过程中,一旦gc发现对象是weakReference可达,就会把它放到ReferenceQueue中,等下次gc时回收它。
WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);
系统为我们提供了WeakHashMap,和HashMap类似,只是其key使用了weak reference。如果WeakHashMap的某个key被垃圾回收器回收,那么entity也会自动被remove。

由于WeakReference被GC回收的可能性较大,因此,在使用它之前,你需要通过weakObj.get()去判断目的对象引用是否已经被回收.

Reference queque

一旦WeakReference.get()返回null,它指向的对象就会被垃圾回收,那么WeakReference对象就没有用了,意味着你应该进行一些清理。比如在WeakHashMap中要把回收过的key从Map中删除掉,避免无用的的weakReference不断增长。
ReferenceQueue可以让你很容易地跟踪dead references。WeakReference类的构造函数有一个ReferenceQueue参数,当指向的对象被垃圾回收时,会把WeakReference对象放到ReferenceQueue中。这样,遍历ReferenceQueue可以得到所有回收过的WeakReference。

Phantom reference

和soft,weak Reference区别较大,它的get()方法总是返回null。这意味着你只能用PhantomReference本身,而得不到它指向的对象。当WeakReference指向的对象变得弱可达(weakly reachable)时会立即被放到ReferenceQueue中,这在finalization、garbage collection之前发生。理论上,你可以在finalize()方法中使对象“复活”(使一个强引用指向它就行了,gc不会回收它)。但没法复活PhantomReference指向的对象。而PhantomReference是在garbage collection之后被放到ReferenceQueue中的,没法复活。

关于Phantom reference的更多讨论,请参考:understanding-weak-references


Heap dump是java进程在特定时间的一个内存快照。通常在触发heap dump之前会进行一次full gc,这样dump出来的内容就包含的是被gc后的对象。


MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。

工具地址 : https://www.eclipse.org/mat/

我在实际操作过程中采用的是jmap获取堆转储文件,然后scp到本地,

加载后首页如下图,在首页上比较有用的是Histogram和Leak Suspects。


1. 用jmap生成堆信息

内存泄漏排查 MAT工具使用

这样在E盘的jmap文件夹里会有一个map.bin的堆信息文件 

然后MAT软件加载

2. 将堆信息导入到mat中分析   

内存泄漏排查 MAT工具使用

加载后首页如下图,在首页上比较有用的是Histogram和Leak Suspects。

内存泄漏排查 MAT工具使用

点击Leak Suspects会在堆转储文件同目录内生成一个Leak Suspects.zip文件,同时也会从首页跳转到Leak Suspects页面。

内存泄漏排查 MAT工具使用

解压该文件后可以通过浏览器打开分析结果

内存泄漏排查 MAT工具使用
下面是Leak Suspects页面

内存泄漏排查 MAT工具使用3. 生成分析

饼图的每个块,代表一个可疑问题,鼠标上去就可以看到下面的内容提示

内存泄漏排查 MAT工具使用

在Leak Suspects页面会给出可能的内存泄露,如上图所示有三个可能的内存泄露,但是只有第一个是我程序里的,另外两个是jar包或jdk里面的,这个可以不用管。

点击Details进入详情页面。在详情页面Shortest Paths To the Accumulation Point表示GC root到内存消耗聚集点的最短路径,如果某个内存消耗聚集点有路径到达GC root,则该内存消耗聚集点不会被当做垃圾被回收。

内存泄漏排查 MAT工具使用

在All Accumulated Objects by Class列举了该对象所存储的所有内容。

内存泄漏排查 MAT工具使用

为了找到内存泄露,我获取了两个堆转储文件,两个文件获取时间间隔是一天(因为内存只是小幅度增长,短时间很难发现问题)。对比两个文件的对象,通过对比后的结果可以很方便定位内存泄露。

MAT同时打开两个堆转储文件,分别打开Histogram,如下图。在下图中方框1按钮用于对比两个Histogram,对比后在方框2处选择Group By package,然后对比各对象的变化。不难发现heap3.hprof比heap6.hprof少了64个eventInfo对象,如果对代码比较熟悉的话想必这样一个结果是能够给程序员一定的启示的。而我也是根据这个启示差找到了最终内存泄露的位置。

内存泄漏排查 MAT工具使用

我内存泄露位置是一个list,这个list只在这里一直不停的往里添加eventInfo对象,却没有释放过

内存泄漏排查 MAT工具使用

修改后代码:

内存泄漏排查 MAT工具使用

   

    从上图可以看到它的大部分功能,在饼图上,你会发现转储的大小和数量的类,对象和类加载器。
正确的下面,饼图给出了一个印象最大的对象转储。移动你的鼠标一片看到对象中的对象的细节检查在左边。下面的Action标签中:

  • Histogram可以列出内存中的对象,对象的个数以及大小。

  • Dominator Tree可以列出那个线程,以及线程下面的那些对象占用的空间。

  • Top consumers通过图形列出最大的object。

  • Leak Suspects通过MA自动分析泄漏的原因。

Histogram

    内存泄漏排查 MAT工具使用

  • Class Name : 类名称,java类名

  • Objects : 类的对象的数量,这个对象被创建了多少个

  • Shallow Heap :一个对象内存的消耗大小,不包含对其他对象的引用

  • Retained Heap :是shallow Heap的总和,也就是该对象被GC之后所能回收到内存的



总结出来只有一条: 存在无效的引用! 

良好的模块设计以及合理使用设计模式有助于解决此问题。