关于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
2
// 其中-z表示忽略除app堆以外的堆(Image/Zygote)
hprof-conv [-z] infile outfile

hprof-conv具体做了什么呢?接下来通过hprof-conv的源码来分析

修改版本信息


首先判断当前hprof文件版本号是否是”JAVA PROFILE 1.0.3”,如果不是则报错,反之将版本信息从”JAVA PROFILE 1.0.3”降为”JAVA PROFILE 1.0.2”。

修改HeapTag信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
while (len > 0) {
unsigned char subType = buf[0];
int justCopy = TRUE;
int subLen;
DBUG("--- 0x%02x ", subType);
switch (subType) {
/* 1.0.2 types */
case HPROF_ROOT_UNKNOWN:
subLen = kIdentSize;
break;
case HPROF_ROOT_JNI_GLOBAL:
subLen = kIdentSize * 2;
break;
case HPROF_ROOT_JNI_LOCAL:
subLen = kIdentSize + 8;
break;
case HPROF_ROOT_JAVA_FRAME:
subLen = kIdentSize + 8;
break;
case HPROF_ROOT_NATIVE_STACK:
subLen = kIdentSize + 4;
break;
case HPROF_ROOT_STICKY_CLASS:
subLen = kIdentSize;
break;
case HPROF_ROOT_THREAD_BLOCK:
subLen = kIdentSize + 4;
break;
case HPROF_ROOT_MONITOR_USED:
subLen = kIdentSize;
break;
case HPROF_ROOT_THREAD_OBJECT:
subLen = kIdentSize + 8;
break;
case HPROF_CLASS_DUMP:
subLen = computeClassDumpLen(buf+1, len-1);
break;
case HPROF_INSTANCE_DUMP:
subLen = computeInstanceDumpLen(buf+1, len-1);
// 如果当前堆被忽略(带了-z参数)则丢弃该tag
if (heapIgnore) {
justCopy = FALSE;
}
break;
case HPROF_OBJECT_ARRAY_DUMP:
subLen = computeObjectArrayDumpLen(buf+1, len-1);
// 如果当前堆被忽略(带了-z参数)则丢弃该tag
if (heapIgnore) {
justCopy = FALSE;
}
break;
case HPROF_PRIMITIVE_ARRAY_DUMP:
subLen = computePrimitiveArrayDumpLen(buf+1, len-1);
// 如果当前堆被忽略(带了-z参数)则丢弃该tag
if (heapIgnore) {
justCopy = FALSE;
}
break;
/* these were added for Android in 1.0.3 */
// 以下为Android新加HeapTag
case HPROF_HEAP_DUMP_INFO:
// 如果带了-z参数则丢弃HPROF_HEAP_ZYGOTE和HPROF_HEAP_IMAGE堆的数据
heapType = get4BE(buf+1);
if ((flags & kFlagAppOnly) != 0
&& (heapType == HPROF_HEAP_ZYGOTE || heapType == HPROF_HEAP_IMAGE)) {
heapIgnore = TRUE;
} else {
heapIgnore = FALSE;
}
justCopy = FALSE;
subLen = kIdentSize + 4;
// no 1.0.2 equivalent for this
break;
case HPROF_ROOT_INTERNED_STRING:
// 将tag名改为HPROF_ROOT_UNKNOWN
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_ROOT_FINALIZING:
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_ROOT_DEBUGGER:
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_ROOT_REFERENCE_CLEANUP:
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_ROOT_VM_INTERNAL:
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_ROOT_JNI_MONITOR:
/* keep the ident, drop the next 8 bytes */
buf[0] = HPROF_ROOT_UNKNOWN;
justCopy = FALSE;
ebAddData(pOutBuf, buf, 1 + kIdentSize);
subLen = kIdentSize + 8;
break;
case HPROF_UNREACHABLE:
buf[0] = HPROF_ROOT_UNKNOWN;
subLen = kIdentSize;
break;
case HPROF_PRIMITIVE_ARRAY_NODATA_DUMP:
// 将空array tag改为array tag且将长度设置为0
buf[0] = HPROF_PRIMITIVE_ARRAY_DUMP;
buf[5] = buf[6] = buf[7] = buf[8] = 0; /* set array len to 0 */
subLen = kIdentSize + 9;
break;
/* shouldn't get here */
default:
fprintf(stderr, "ERROR: unexpected subtype 0x%02x at offset %zu\n",
subType, (size_t) (buf - origBuf));
goto bail;
}
if (justCopy) {
/* copy source data */
DBUG("(%d)\n", 1 + subLen);
ebAddData(pOutBuf, buf, 1 + subLen);
} else {
/* other data has been written, or the sub-record omitted */
DBUG("(adv %d)\n", 1 + subLen);
}
/* advance to next entry */
buf += 1 + subLen;
len -= 1 + subLen;
}

主要做了4件事

  1. 针对标准hprof的HeapTag除了HPROF_INSTANCE_DUMP、HPROF_OBJECT_ARRAY_DUMP、HPROF_PRIMITIVE_ARRAY_DUMP照常写入,不做更改

  2. 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

  3. 丢弃HPROF_HEAP_DUMP_INFO Tag不写入,同时根据命令是否携带-z参数来判断当前堆是否需要写入,更新heapIgnore标志位

  4. 针对HPROF_INSTANCE_DUMP、HPROF_OBJECT_ARRAY_DUMP、HPROF_PRIMITIVE_ARRAY_DUMP,如果当前堆不需要写入,即heapIgnore = TRUE,则丢弃当前Record信息

参考资料