本系列笔记主要基于《深入理解 Java 虚拟机: JVM 高级特性与最佳实践 第 2 版》, 是这本书的读书笔记.
MAT 是分析 Java 堆内存的一个工具, 全称是 The Eclipse Memory Analyzer Tool, 用来帮助分析内存泄漏和减少内存消耗. 使用 MAT 分析 Java 堆快照, 可以快速计算出对象的保留大小(Retained Sizes), 查找到阻止对象被回收的原因, MAT 会自动生成一个包含内存泄漏疑点的报告.
MAT 可以从 Eclipse 网站下载: http://www.eclipse.org/mat/ https://www.eclipse.org/mat/
生成 Dump
使用 MAT 分析的是 Heap Dump, 也就是堆内存快照, 生成快照有以下几种方式:
使用虚拟机参数 - XX:+HeapDumpOnOutOfMemoryError, 溢出时自动生成快照.
使用 jmap 命令, jmap -dump:format=b,file=${dir}/jmap.hprof pid
使用 MAT 导出本地 java 进程的内存快照, File->Acquire Heap Dump->选择要 dump 的 java 进程就可以了.
MAT 的使用
生成完 dump 之后, 可以用 MAT 打开 dump 出来的快照文件, File -> Open Heap Dump, 对 dump 文件进行分析, 生成一个 Overview 视图:
首先会列出堆内存的大小, 有多少个类, 有多少个对象, 以及多少个类加载器.
然后是一个根据对象的 Retained Size 大小形成的饼状图, 鼠标放上去, 左侧 Inspector 视图会显示这个对象的详细信息.
然后是其他功能, 比如常用的图表, Histogram 直方图, Dominator Tree 支配树, 还会生成一个分析报告 Leak Suspects Report.
基础概念
继续分析之前, 先了解几个基础概念.
Shallow Heap 和 Retained Heap
Shallow Heap 表示对象本身占用内存的大小, 不包含对其他对象的引用, 也就是对象头加成员变量 (不是成员变量的值) 的总和.
Retained Heap 是该对象自己的 Shallow Heap, 并加上从该对象能直接或间接访问到对象的 Shallow Heap 之和. 换句话说, Retained Heap 是该对象 GC 之后所能回收到内存的总和.
把内存中的对象看成下图中的节点, 并且对象和对象之间互相引用. 这里有一个特殊的节点 GC Roots, 这就是 reference chain 的起点.
从 obj1 入手, 上图中蓝色节点代表仅仅只有通过 obj1 才能直接或间接访问的对象. 因为可以通过 GC Roots 访问, 所以左图的 obj3 不是蓝色节点; 而在右图却是蓝色, 因为它已经被包含在 retained 集合内. 所以对于左图, obj1 的 retained size 是 obj1,obj2,obj4 的 shallow size 总和; 右图的 retained size 是 obj1,obj2,obj3,obj4 的 shallow size 总和. obj2 的 retained size 可以通过相同的方式计算.
对象引用 Reference
关于对象的引用, 前面的文章讲到过, 划分如下:
强引用 (Strong Reference) 就是在代码中普遍存在的, 类似 "Object obj = new Object()" 这类的引用, 只要强引用还存在, 垃圾收集器永远不会回收被引用的对象.
软引用 (Soft Reference) 是用来描述有用非必需的对象. 软引用关联的对象, 在系统将要发生内存溢出之前, 将会对这些对象进行二次回收. 如果这次回收后还没有足够的内存, 才会抛出内存溢出异常. 上面所说的 "食之无味, 弃之可惜" 的对象就是属于软引用.
弱引用 (Weak Reference) 是用来描述非必需的对象, 但是比软引用更弱一些, 弱引用关联的对象只能生存到下一次垃圾收集发生之前. 当下一次垃圾收集时, 无论内存是否足够, 都会回收掉被弱引用关联的对象.
虚引用 (Phantom Reference) 也称为幽灵引用或者幻影引用, 它是最弱的一种引用. 一个对象是否有虚引用存在, 完全不会对其生存时间造成任何影响, 也无法通过虚引用获得一个对象实例. 为对象设置虚引用的目的, 就是能在这个对象被收集器回收时收到一个系统通知.
四种引用中, 只有强引用是强可达性, 根据可达性分析回收内存时, 永远不会被回收.
GC Roots 和 引用链
JVM 在进行 GC 的时候是通过使用可达性来判断对象是否存活, 通过 GC Roots(GC 根节点)的对象作为起始点, 从这些节点开始进行向下搜索, 搜索所走过的路径成为 Reference Chain(引用链), 当一个对象到 GC Roots 没有任何引用链相连 (用图论的话来说就是从 GC Roots 到这个对象不可达) 时, 则证明此对象是不可用的.
如下图所示, 对象 Object 5,Object 6,Object 7 虽然互相关联, 但是它们到 GC Roots 是不可达的, 所以它们将被判定为可回收的对象:
在 Java 中, 可作为 GC Roots 的对象有以下几种:
虚拟机栈 (栈帧中的本地变量表) 中引用的对象.
方法区中类静态属性引用的对象.
方法区中常量引用的对象.
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象.
四种引用, GC Roots 以及引用链, 可以参考之前的博客文章:《JVM 探秘: 四种引用, 对象的生存与死亡》 http://www.cellei.com/blog/2018/04161
Histogram 直方图
点击工具栏上的 图标, 打开 Histogram 直方图视图, 可以列出每个类产生的实例数量, 以及所占用的内存大小和百分比. 主界面如下图所示:
图中 Shallow Heap 和 Retained Heap 分别表示对象自身不包含引用的大小和对象自身并包含引用的大小, 具体请参考下面 Shallow Heap 和 Retained Heap 部分的内容. 默认的大小单位是 Bytes, 可以在 Windows - Preferences 菜单中设置单位, 图中设置的是 KB.
通过直方图视图可以很容易找到占用内存最多的几个类(通过 Retained Heap 排序), 还可以通过其他方式进行分组(见下图):
如果存在内存溢出, 时间久了溢出类的实例数量或者内存占比会越来越多, 排名也越来越靠前. 可以点击工具类上的 图标进行对比, 通过多次对比不同时间点下的直方图对比就很容易把溢出的类找出来.
Dominator Tree 支配树
点击工具栏上的 图标可以打开 Dominator Tree(支配树)视图, 在此视图中列出了每个对象 (Object Instance) 与其引用关系的树状结构, 同时包含了占用内存的大小和百分比.
通过 Dominator Tree 视图可以很容易的找出占用内存最多的几个对象(根据 Retained Heap 或 Percentage 排序), 和 Histogram 类似, 也可以通过不同的方式进行分组显示.
定位溢出源
Histogram 视图和 Dominator Tree 视图的角度不同, 前者是基于类的角度, 后者是基于对象实例的角度, 并且可以更方便的看出其引用关系.
首先, 在两个视图中找出疑似溢出的对象或者类 (可以通过 Retained Heap 排序, 并且可以在 Class Name 中输入正则表达式的关键词只显示指定的类名), 然后右键选择 Path To GC Roots(Histogram 中没有此项) 或 Merge Shortest Paths to GC Roots, 然后选择 exclude all phantom/weak/soft etc. reference:
GC Roots 意为 GC 根节点, 其含义见上面的 GC Roots 和引用链部分, 后面的 exclude all phantom/weak/soft etc. reference 意思是排除虚引用, 弱引用和软引用, 即只剩下强引用, 因为除了强引用之外, 其他的引用都可以被 JVM GC 掉, 如果一个对象始终无法被 GC, 就说明有强引用存在, 从而导致在 GC 的过程中一直得不到回收, 最终就内存溢出了.
通过结果就可以很方便的定位到具体的代码, 然后分析是什么原因无法释放该对象, 比如被缓存了或者没有使用单例模式等等.
举例, 如果是这样的执行结果:
上图中保留了大量的 VelocitySqlBulder 的外部引用, 后来查看了代码, 原来每次调用的时候都实例化一个新的对象, 由于 VelocitySqlBulder 类是无状态的工具类, 因此修改为单例方式就可以解决这个问题.
后续观察
根据上面分析的结果对问题进行处理之后, 再对照之前的操作, 看看对象是否还再持续增长, 如果没有就说明这个地方的问题已经解决了.
最后再用 jstat 持续跟踪一段时间, 看看 Old 和 Perm 区的内存是否最终稳定在一个范围之内, 如果长时间稳定在一个范围说明溢出问题得到了解决, 否则还要继续进行分析和处理, 一直到稳定为止.
来源: http://www.bubuko.com/infodetail-3398286.html