关于Hprof的方方面面
Hprof概述
hprof最初是由J2SE支持的一种二进制堆转储格式,hprof文件保存了当前java堆上所有的内存使用信息,能够完整的反映虚拟机当前的内存状态。
格式
详细的Hprof标准格式可以参考HPROF Agent,本节将根据该文档简单介绍Hprof格式。
Hprof文件由FixedHead和一系列的Record组成,Record包含字符串信息、类信息、栈信息、GcRoot信息、对象信息。每个Record都是由1个字节的Tag、4个字节的Time、4个字节的Length和Body组成,Tag表示该Record的类型,Body部分为该Record的内容,长度为Length。
| 1 | 4 | 4 | Length |
|---|---|---|---|
| tag | time | length | body |
不同类型Record的body部分格式不同,包含的信息也不同,接下来分析一些比较重要的Record。
FixedHead
Hprof首先包含了文件的版本描述信息及版本号,一般固定为”JAVA PROFILE 1.0.2”,以/0结尾。随后是长度为4字节的id size,最后是长度为8个字节的时间戳。Hprof文件中几乎所有类型的Record都有用到id,这里的id size指的就是这个id的长度,用来告诉我们需要读取多少字节的内容来获取id。
| 19 | 4 | 8 |
|---|---|---|
| “JAVA PROFILE 1.0.2/0” | ID size | 时间戳 |
StringTable
FixedHead后面就是一系列的String Record,Tag为0x01,每个String Record保存一个字符串和该字符串对应的id,后续通过该id来查询字符串。这里的String存储了后面将用到字符串,包括类名,常量等。String Record的body首先包含一个id,由于body部分长度为Length,所以字符串内容为后续(Length-idSize)个字节。
LOAD CLASS
LOAD CLASS Record记录了简单的类信息,Tag为0x02。body部分包含了4个字节长的序列号、类id、4个字节的栈序列号、类名id,可以通过类名id从上述的一系列String Record中找到当前类的类名。
HEAP DUMP/HEAP DUMP SEGMENT
这两个Record主要是包含GcRoot信息、对象信息、详细的类信息,在解析Hprof的时候这两个Record对我们来说作用是一样的(不知道为啥分成两个Record?),接下来就以HEAP DUMP为例。
HEAP DUMP是一个非常大的Record,内部包含了一系列的SubRecord,每个SubRecord均以一个字节的SubTag开头,接下来一一解析这些SubRecord。
ROOT XXX
首先是一系列的GCRoot,SubTag范围是 0x01到0x08 和 0xFF 这9个,每个GCRoot的Record均有一个Object Id,用来表示该GCRoot对应的Object,其余的部分参考文档。
CLASS DUMP
CLASS DUMP包含详细的类信息,SubTag为0x20,接下来按照顺序来解析body部分的内容
- class object ID:与上面的LOAD CLASS中的类id一致,所以可以通过之前的LOAD CLASS Record来得到当前类的类名
- stack trace serial number:4个字节的栈序列号
- super class object ID:父类的类id
- class loader object ID:类加载器的Object id
- signers object ID:暂不清楚作用
- protection domain object ID:暂不清楚作用
- reserved:两个reserved Id,暂无用处
- instance size (in bytes):四个字节的类实例占用的大小
- constant pool:2个字节的常量池数量n,接下来包含n个常量,每一个常量包含两个字节的index、1个字节的类型信息、x个字节的常量值。其中x根据该常量的类型决定,Object类型的值为一个Object id,具体参考文档。
- 静态变量信息:和constant pool十分类似,唯一区别是2个字节的index替换为变量Name id,用来表示该静态变量的变量名
- 成员变量信息:首先也是2个字节的数量n,接下来是n个成员变量,每一个成员变量包含一个Name id和1个字节的变量类型
综上,CLASS DUMP包含了类的类加载器、父类、实例大小、常量池、静态变量、成员变量的信息。
INSTANCE DUMP
NSTANCE DUMP包含类对象的信息,SubTag为0x21。包含了一个Object id、4个字节的栈序列号、类id、4个字节的长度n、成员变量信息。其中长度n表示后面的成员变量信息占用了多少字节。成员变量信息是该对象所有成员变量的值组合在一起(包括父类的成员变量),读取方式如下:
- 从上述CLASS DUMP中获得当前类的成员变量信息,按照该信息顺序依次读取变量值(比如Char读取两个字节的长度,Object读取idSize的长度)
- 获得父类的成员变量信息,重复上述操作,直至没有父类为止
OBJECT ARRAY DUMP 和 PRIMITIVE ARRAY DUMP
这两个SubRecord存储数组对象的信息,比较简单,可以参考文档自行分析。
Android中的hprof
在Android设备上,我们可以通过两种方式生成当前进程的hprof文件
- 通过调用
Debug.dumpHprofData(String filePath)方法来生成hprof文件 - 通过执行shell命令
adb shell am dumpheap pid /data/local/tmp/x.hprof来生成指定进程的hprof文件到目标目录
然而Android平台上的hprof文件和标准Java的hprof定义有一些区别,主要是版本号不一样,而且增加了一些特殊Tag,在art/runtime/hprof/hprof.cc中有如下定义
可以看出Android增加了额外的9个HeapTag,其中比较重要的是HPROF_HEAP_DUMP_INFO。Android上将java堆分为Heap-App、Heap-Image、Heap-Zygote三块,这个Tag的作用是切换当前的堆,该Tag后面紧跟着一个4个字节的堆id和一个堆名称id。比如出现一个HPROF_HEAP_DUMP_INFO Record,且该Record表示Heap-Image,那么表示后续所有Record中的类、对象、GCRoot对象均在Heap-Image中存储,直到下一个HPROF_HEAP_DUMP_INFO出现为止。
Android平台上针对hprof的改动会导致MAT等标准hprof分析工具无法解析,因此我们需要使用AndroidSDK提供的hprof-conv工具将hprof转换为标准hprof,该工具在sdk/platform-tools下,使用方式如下
1 | // 其中-z表示忽略除app堆以外的堆(Image/Zygote) |
hprof-conv具体做了什么呢?接下来通过hprof-conv的源码来分析
修改版本信息

首先判断当前hprof文件版本号是否是”JAVA PROFILE 1.0.3”,如果不是则报错,反之将版本信息从”JAVA PROFILE 1.0.3”降为”JAVA PROFILE 1.0.2”。
修改HeapTag信息
1 | while (len > 0) { |
主要做了4件事
针对标准hprof的HeapTag除了HPROF_INSTANCE_DUMP、HPROF_OBJECT_ARRAY_DUMP、HPROF_PRIMITIVE_ARRAY_DUMP照常写入,不做更改
Android新增的Tag除了HPROF_HEAP_DUMP_INFO、HPROF_PRIMITIVE_ARRAY_NODATA_DUMP,都将Tag改为HPROF_ROOT_UNKNOWN写入,同时将HPROF_PRIMITIVE_ARRAY_NODATA_DUMP改为长度为0的HPROF_PRIMITIVE_ARRAY_DUMP
丢弃HPROF_HEAP_DUMP_INFO Tag不写入,同时根据命令是否携带-z参数来判断当前堆是否需要写入,更新heapIgnore标志位
针对HPROF_INSTANCE_DUMP、HPROF_OBJECT_ARRAY_DUMP、HPROF_PRIMITIVE_ARRAY_DUMP,如果当前堆不需要写入,即heapIgnore = TRUE,则丢弃当前Record信息