0 序
- 缘起
- 近期项目上我负责的微服务出现了难以排查的问题,目前怀疑是 JVM线程方面的情况,但也需从
heap dump
方面进一步印证,故需深入了解heap dump
文件内容的各项含义。- 本文主要转载了网友的观点,详见:参考文献
1 Heap Dump 分析
1.1 heapdump 简介
- heapdump文件是一个二进制文件,它保存了某一时刻JVM堆的对象使用情况。
- heapdump文件是指定时刻的Java堆栈的快照,是一种镜像文件。
- Heap Dump中主要包含当生成快照堆中的java对象和类的信息,主要分为如下几类:
- 对象信息:类名、属性、基础类型和引用类型
- 类信息:类加载器、类名称、超类、静态属性
- gc roots:JVM中的一个定义,进行垃圾收集时,要遍历可达对象的起点节点的集合
- 线程栈和局部变量:快照生成时候的线程调用栈,和每个栈上的局部变量
1.2 heapdump 用途
- heapdump是诊断与【JVM内存】相关的问题的重要手段,例如:内存泄漏、垃圾回收问题和java.lang.OutOfMemoryError。同时也是优化内存消耗的重要手段。
1.3 JVM内存结构(简介)
说起heapdump,了解jvm 的内存结构,会更有助于对heapdump的使用。
JVM定义了若干个程序执行期间使用的数据区域。这个区域里的一些数据在JVM启动的时候创建,在JVM退出的时候销毁。而其他的数据依赖于每一个线程,在线程创建时创建,在线程退出时销毁。
jvm结构概览如下:(各块区域详细解释不在此说明,百度即可查到)
- JVM内存模型中的这些区域,都是有大小限制的,当然也可以通过JVM提供的参数来设置这些区域所占内存的大小。
运行时各区块的描述如下
-Xms :初始堆大小(默认物理内存1/64);-Xmx :最大堆大小(默认物理内存1/4).。
-Xss:表示每个线程栈的大小。
-Xmn:表示新生代(年轻代)的大小
-XX:NewRatio:默认为2,表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认为8,表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Full Gc的初始阈值(元空间无固定初始大小), 以字节为单位。
1.4 JVM内存为何会溢出?
- JVM根据generation(代)来进行GC,绝大多数的对象都在young generation被分配,也在young generation被收回,当young generation的空间被填满,GC会进行minor collection(次回收),速度非常快。
- 其中,young generation中未被回收的对象被转移到tenured generation,当tenured generation被填满时,即触发major collection(FULL GC主回收),整个应用程序都会停止下来直到回收完成。
- 因此,产生内存溢出错误原因一般出于以下原因:
- JVM内存过小,或配置不合理
- 程序内存泄露导致的对象无法回收
- 产生的对象超过了超过了堆的大小
1.5 如何生成、导出heapdump?
1.5.1 方式1:命令生成
jmap -dump:live,format=b,file=heapdump.hprof <pid>
# 如下命令亦可
jcmd <pid> GC.heap_dump heapdump.hprof
最近的实际项目中我是这么用的:
jcmd {PID} VM.uptime
jcmd {PID} GC.heap_dump heap-dump-{PID}-$(date +'%Y%m%d%H%M%S').hprof
jcmd {PID} Thread.print > thread-dump-{PID}-$(date +'%Y%m%d%H%M%S').tdump
jcmd {PID} PerfCounter.print > perf-counter-print-{PID}-$(date +'%Y%m%d%H%M%S').txt
1.5.2 方式2:配置Java 启动参数生成
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tmp/heapdump.hprof
2 heap dump文件分析
2.1 基于 MAT 工具的heap dump分析
2.1.1 MAT 工具简介
- mat(Eclipse Memory Analyzer tool),是一个快速且功能丰富的Java堆分析器,可帮助您查找内存泄漏并减少内存消耗。
- 使用Memory Analyzer分析具有数亿个对象的高效堆转储,快速计算对象的保留大小,查看谁阻止垃圾收集器收集对象,运行报告以自动提取泄漏嫌疑者。
2.1.2 术语解释
在使用mat 前,先了解一些术语,便于工具的使用。
- Shallow heap:一个对象本身占用的堆内存大小,也就是对象头加成员变量(不是成员变量的值)的总和。
- 如:一个对象中,每个引用占用8或64位,Integer占用4字节,Long占用8字节等等。
- Retained Heap:如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。
- 即对象被垃圾回收器回收后能被GC从内存中移除的所有对象之和。
- 相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(若该对象释放,retained heap都可以被释放)。
- gc root: 在java语言中,都是通过可达性分析来判定对象是否存活的。
- 此算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可达的,因此能被GC 回收掉。
- 因此,可以得出,只有引用类型的变量才被认为是Roots,值类型的变量永远不被认为是Roots。
- GC ROOT的目标对象是要以当前还在存活的对象集合。
- 因此,必须要选取确定存活的引用类型对象,GC管理的区域是java的堆,虚拟机栈、方法区和本地方法栈不被GC所管理。
- 因此,选用这些区域内引用的对象作为GC Roots,是不会被GC回收的。
- 此算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可达的,因此能被GC 回收掉。
2.1.3 MAT 功能模块
常用到的功能包括:
- Histogram直方图
- Dominator Tree
- Top Components
- Top Consumers
- Leak Suspects
如下进行逐一介绍。 打开dump后概览图如下:
为方便后续功能理解使用,先阐述几个通用功能
- List object:其下有with outgoing references,with incoming references两个选项。
with outgoing references:查看当前对象持有的外部对象引用(在对象关系图中为从当前对象指向外的箭头)
with incoming references:查看当前对象被哪些外部对象所引用(在对象关系图中为指向当前对象的箭头)
- Paths to GC Roots:从当前对象到GC roots的路径,这个路径解释了为什么当前对象还能存活,对分析内存泄露很有帮助,这个查询只能针对单个对象使用。其下有很多选项,在查询到GC root的路径时,是包含所有引用,还是排除一些类型的引用(如软引用、弱引用、虚引用),从GC角度说,一个对象无法被GC,一定是因为有强引用存在,其它引用类型在GC需要的情况下都是可以被GC掉的,所以可以使用 exclude all phantom/weak/soft etc. references 只查看GC路径上的强引用
2.1.3.1 Histogram / 直方图
Histogram:直方图,可以列出内存中的对象,对象的个数以及大小。
该视图以Class类的维度展示每个Class类的实例存在的个数、 占用的 [Shallow内存] 和 [Retained内存] 大小,可以分别排序显示。
从Histogram视图可以看出,哪个Class类的对象实例数量比较多,以及占用的内存比较大,Shallow Heap与Retained Heap的区别会在后面的概念介绍中说明。
不过,多数情况下,在Histogram视图看到实例对象数量比较多的类都是一些基础类型,如char[]、String、byte[],所以仅从这些是无法判断出具体导致内存泄露的类或者方法的,可以使用 List objects或 Merge Shortest Paths to GC roots 等功能继续钻取数据。
如果Histogram视图展示的数量多的实例对象不是基础类型,是有嫌疑的某个类,如项目代码中的bean类型,那么就要重点关注了。
2.1.3.2 Dominator Tree / 支配树
Dominator Tree:支配树,可以列出那个线程,以及线程下面的那些对象占用的空间。
该视图以实例对象的维度展示当前堆内存中Retained Heap占用最大的对象,以及依赖这些对象存活的对象的树状结构
视图中展示了实例对象名、Shallow Heap大小、Retained Heap大小、以及当前对象的Retained Heap在整个堆中的占比
Dominator Tree支配树可以很方便的找出占用Retained Heap内存最多的几个对象,并表示出某些objects的是因为哪些objects的原因而存活,在之后的 Dominator Tree概念 部分会对支配树做更详细的说明和举例
2.1.3.3 Top consumers
Top consumers:通过图形列出最大的object
可以通过按包名查看区分占用,根据包我们知道哪些公共用的到jar或自己的包占用
2.1.3.4 Thread Overview / 线程概览
在Thread Overview视图可以看到:线程对象/线程栈信息、线程名、Shallow Heap、Retained Heap、类加载器、是否Daemon线程等信息
在分析内存Dump的MAT中还可以看到线程栈信息,这本身就是一个强大的功能,类似于jstack命令的效果
而且还能结合内存Dump分析,看到线程栈帧中的本地变量,在左下方的对象属性区域还能看到本地变量的属性,真的很方便
2.1.3.5 Leak Suspects / 泄露猜想
Leak Suspects通过MA自动分析泄漏的原因
- Leak Suspects 是MAT帮我们分析的可能有内存泄露嫌疑的地方,可以体现出哪些对象被保持在内存中,以及为什么它们没有被垃圾回收。MAT工具分析了heap dump后在界面上非常直观的展示了一个饼图,该图深色区域被怀疑有内存泄漏,
- 接下来是一个简短的描述,告诉我们哪些线程占用了大量内存,并且明确指出system class loader加载的实例有内存聚集,并建议用关键字对应进行检查。在下面还有一个“Details”链接,可以查看明细信息。
- (1)Details的最开始是Description描述,和前一个页面对内存泄露嫌疑点的描述一致,下面有一些与怀疑的内存泄露点关联的查询结果展示,是分析报告中认为可能会存在问题,协助我们深入分析问题根源的。
- (2)Shortest Paths To the Accumulation Point:当前对象的 Path to GC roots,即到GC roots的路径。作用是可以分析是由于和哪个GC root相连导致当前Retained Heap占用相当大的对象无法被回收。
- (3)Accumulated Objects in Dominator Tree:以对象的维度展示了以当前对象为根的 Dominator Tree支配树,可以方便的看出受当前对象“支配”的对象中哪个占用Retained Heap比较大。
- (4)Accumulated Objects by Class in Dominator Tree:展示了以当前对象为根的Dominator Tree支配树,并以Class类分组。
- (5)Thread Detail:Detail明细的最后由于当前怀疑泄露点为main Thread线程对象,故展示了线程明细信息,调用栈信息,对分析内存溢出的发生位置很有帮忙