[JVM] Java Heap Dump 分析 [转载]

发布时间 2023-12-07 15:16:07作者: 千千寰宇

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主回收),整个应用程序都会停止下来直到回收完成。
  • 因此,产生内存溢出错误原因一般出于以下原因:
  1. JVM内存过小,或配置不合理
  2. 程序内存泄露导致的对象无法回收
  3. 产生的对象超过了超过了堆的大小

1.5 如何生成、导出heapdump?

1.5.1 方式1:命令生成

jmap -dump:live,format=b,file=heapdump.hprof <pid>

# 如下命令亦可
jcmd <pid> GC.heap_dump heapdump.hprof

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回收的。

2.1.3 MAT 功能模块

常用到的功能包括:

  • Histogram直方图
  • Dominator Tree
  • Top Components
  • Top Consumers
  • Leak Suspects

如下进行逐一介绍。 打开dump后概览图如下:

为方便后续功能理解使用,先阐述几个通用功能

  1. List object:其下有with outgoing references,with incoming references两个选项。

with outgoing references:查看当前对象持有的外部对象引用(在对象关系图中为从当前对象指向外的箭头)
with incoming references:查看当前对象被哪些外部对象所引用(在对象关系图中为指向当前对象的箭头)

  1. 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 objectsMerge 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线程对象,故展示了线程明细信息,调用栈信息,对分析内存溢出的发生位置很有帮忙

X 参考文献