1.前提
本文将以讲解原理为主,部分代码讲解,可以自行下载HAHA库源码进行对照学习。
implementation 'com.squareup.haha:haha:2.0.3'
如果你看过Leakcanary2.0之前的源码,你应该会熟悉HAHA库,它是一个解析hrpof文件的库,通过它我们可以获得当前内存的对象信息,包括如下信息。
1.获得对象的shallow size和retained size。
2.获得对象的引用链。
2.HAHA库的实现步骤:
1.Hprof(堆转存)文件的解析。
2.支配树的构造和Retained Size计算。
3.最短路径构建。
下面我将会从这几个主要维度进行介绍。
2.1 Hprof(堆转存)文件的解析。
Hprof是一份存储了当前时刻内存信息的文件,即内存快照。可以方便我们进行内存分析。里面包括类信息、栈信息、堆信息。其中堆信息是需要我们关注的重点。堆信息包括了所有的对象:线程对象、类对象、实例对象、对象数组对象、原始类型数组对象。其中类对象可以获得该类的常量、静态变量、实例变量等信息。
2.1.1 Hprof 文件格式
首先需要理解Hprof文件的格式,它的基本数据类型为u1、u2、u4、u8,分别代表1 byte、2 byte、4 byte、8 byte的内容,由文件头和文件内容(records)组成,如下图:
2.1.2 文件头格式如下:
长度 | 含义 |
---|---|
[u1]* | 以null为结尾的一串字节,表示版本号名称及相应的版本,比如JAVA PROFILE 1.0.3(一般是18byte + 1byte null字节) |
u4 | 标识符大小,这些标识符代表UTF8的字符串、对象、堆栈等信息的id长度 |
u4 | 高位时间戳 |
u4 | 低位时间戳 |
2.1.3 文件头格式如下:
文件内容格式如下:
长度 | 含义 |
---|---|
u1 | TAG值,16进制地址,代表它是一个什么类型的信息,它是一个16进制的值,比如strings、堆信息等record |
u4 | 时间戳 |
u4 | LENGTH,即BODY的字节长度 |
[u1]* | BODY,具体内容,即上述读取出的字节长度 |
2.1.4 文件内容格式如下:
string的格式如下,其他信息可以自行查阅:http://hg.openjdk.java.net/jdk6/jdk6/jdk/raw-file/tip/src/share/demo/jvmti/hprof/manual.html#mozTocId848088
2.1.5 TAG类型信息:
HPROF_TAG_STRING = 0x01;字符串信息
HPROF_TAG_LOAD_CLASS = 0x02; 可以获得类id对应的字符串信息
HPROF_TAG_STACK_FRAME = 0x04;栈帧信息
HPROF_TAG_STACK_TRACE = 0x05;栈信息
HPROF_TAG_HEAP_DUMP = 0x0C;
HPROF_TAG_HEAP_DUMP_SEGMENT = 0x1C;这2个是所有的对象信息
如果TAG是HEAP_DUMP 或 HEAP_DUMP_SEGMENT ,它的BODY由一系列子record组成
HEAP DUMP 或 HEAP DUMP SEGMENT的sub-tag信息
// Traditional.
HPROF_ROOT_UNKNOWN = 0xFF,
HPROF_ROOT_JNI_GLOBAL = 0x01, // native 变量
HPROF_ROOT_JNI_LOCAL = 0x02,
HPROF_ROOT_JAVA_FRAME = 0x03,
HPROF_ROOT_NATIVE_STACK = 0x04,
HPROF_ROOT_STICKY_CLASS = 0x05,
HPROF_ROOT_THREAD_BLOCK = 0x06,
HPROF_ROOT_MONITOR_USED = 0x07,
HPROF_ROOT_THREAD_OBJECT = 0x08,
HPROF_CLASS_DUMP = 0x20, // 类
HPROF_INSTANCE_DUMP = 0x21, // 实例对象
HPROF_OBJECT_ARRAY_DUMP = 0x22, // 对象数组
HPROF_PRIMITIVE_ARRAY_DUMP = 0x23, // 基础类型数组
// Android.
HPROF_HEAP_DUMP_INFO = 0xfe,
HPROF_ROOT_INTERNED_STRING = 0x89,
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
HPROF_ROOT_DEBUGGER = 0x8b,
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
HPROF_ROOT_VM_INTERNAL = 0x8d,
HPROF_ROOT_JNI_MONITOR = 0x8e,
HPROF_UNREACHABLE = 0x90, // Obsolete.
HPROF_PRIMITIVE_ARRAY_NODATA_DUMP = 0xc3, // Obsolete.
2.1.6 Basic Type:
在类对象中可以获得所有实例变量的type,通过type我们即可以知道它是一个什么类型。
长度 | 含义 |
---|---|
2 | object |
4 | boolean |
5 | char |
6 | float |
7 | double |
8 | byte |
9 | short |
10 | int |
11 | long |
通常我们关心的主要是三类信息:
- 字符串信息:所有的字符串,包括类名、实例名、变量名等。
- 类的结构信息:父类信息、变量布局等。
- 堆信息:包括GC ROOT、普通对象、普通对象数组、对象之间的引用等。
2.2 GC ROOT
HAHA库就是通过上述规则读取获得类对象、实例对象、对象数组、基本类型数组、GC ROOT等信息。
在所有对象中,会有一部分虚拟机认定的GC Root。
什么是GC Root:首先我们需要了解一下JVM的垃圾回收算法,其中一个是可达性算法,可达性的意思是如果存在从一个起点到该对象的引用链,该对象就不能被回收,该起点就是GC Root。
GC Root的特点:当前时刻存活的对象!这是肯定的,只有引用是活跃的,那么它所直接引用和间接引用的对象必然是有用的,是不能被回收的。
可以被当成GC root的类型:
- Stack Local:当前活跃的栈帧中指向GC堆中的引用(局部变量、方法的引用类型参数)
- Thread:存活的线程
- JNI Local:Native栈中的局部变量
- JNI Global:Native全局引用
- Monitor Used:用于同步的监控对象
- Held by JVM:用于JVM特殊目的由GC保留的对象。包括类加载器、JVM中重要的异常类等。
3.构建支配树:
在Hprof这份文件中,将解析出来的对象之间建立一个引用与被引用的关系,然后再为所有的GC ROOT设置一个超级源点,这样就会形成一个以超级源点为根的引用树(中间可能会存在环)。
3.1 Shallow Size 和 Retained Size
3.1.1 Shallow Size:
某个对象的Shallow Size就是对象本身的大小,不包含引用的大小。
下面是对象的内存结构:包括下面几个部分:
- 对象头:包括Mark Word和Klass Point(指针,指向方法区的Class对象信息),下面还分三种大小
- 在32位系统下,Mark Word是4byte,指针大小是4byte,所以对象头为8byte
- 在64位系统下,Mark Word是8byte,指针大小是8byte,所以对象头为16byte
- 在64位系统下并开启指针压缩的情况下,Mark Word是8byte,指针大小是4byte,所以对象头为12byte
- 实际数据:就是实例对象的类型大小,比如int,String等大小
对齐填充:这个并不一定会存在,因为要求对象起始地址必须是8字节的整数倍。
比如下面这个类:
public class A {
int a;
B b;
}
在64位系统并开启指针压缩的情况下,它的Shallow Size = 12 (对象头)+ 8(实际大小)+4(对齐填充)= 24byte。
3.1.2 Retained Size:
Retained Size的大小简单来理解就是其支配的所有节点的Shallow Size之和,这里有一个支配的节点的概念,在对象中的应用就是对象A->B->C,这中间没有任何引用指向B或者C,这就是A支配B,B支配C,A支配C。
比如下面这张图,我们来求对象A的Retained Size,对象A有实例对象B、C、D,正常来说就是B、C、D的Shallow Size之和就是它的Retained Size,但是对象D(D有可能是一个全局的实例)也被对象E所引用,所以对象D的支配点就不是A而是最顶层的O,因此对象A的Retained Size = B + C的Retained Size。
换句话说Retained Size就是某对象被VM回收后所能释放的内存大小。
Retained Size对于内存的分析有很大的指引作用,该值大有可能是不合理的内存使用或者泄露。
3.2 支配树
再来看一下支配树的定义:对于有向图D,起点S,终点T,存在S>多条路径>T,在所有的路径中S到T必经的点称为支配点,删了该点,将不存在S到T的路径,因此称起点为S时,该点支配了T。离T点最近的支配点称为直接支配点。
S>T中可能有很多支配点,由这些支配点构成的树,称为支配树
支配树的性质:
- S为树根
- 根到树上任一点的路径经过的点均为根支配的点
任意子树也有上述性质。
有木有觉得和求Reatined Size的流程很像,也就是说,在求某个对象的Reatined Size,只要构建相应的支配树,然后进行其支配节点的相加即可。
支配树的生成:
一般来说,对于一张DAG(有向无环图)来说,可以通过拓扑序+LCA(最近公共祖先)的方法来构造支配树。但是对于实际对象的引用关系,有可能会存在环,也就是普通的有向图。下面我先介绍一下DAG构造支配树的过程:
3.3 拓扑排序
定义:它是对有向图的顶点排成一个线性序列。
在图论中,拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列。且满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面。
-
拓扑序可以有多个。
拓扑序的求得方式可以自行查资料,我这里只说结果。求拓扑序中第x点的直接支配点时,该x点前的直接支配点已经求得完成,也就是支配树已经构造好,这时候就可以对点x的所有父亲求LCA求得直接支配点。举个例子,对于下图(数字代表求好的拓扑序号):
比如我们现在想求7的直接支配点,则说明1-6的支配树已经构造完成,只需要求7所有父类的LCA即可
。即下图:
上面是一般情况,正常来说引用都是存在环的,也就是说在求某个点的直接支配点时,它的有些父亲可能还没有找到自己的直接支配点,这时候子类当然也不能找到它自己的支配点,HAHA库中的做法是:如果某个父类没有支配点(也就是并未处理),就先跳过它,也就是认为少了一个父类,继续当前寻找直接支配点,等整棵树处理完毕以后,再迭代进行支配树的构建,直到所有的节点的支配树都找到且没有改变,至此结束。
第一遍可能看的会有点蒙,我举个例子,比如下面的有向图:序号是已经排好的拓扑序
我们现在求点3的直接支配点,具体步骤如下:
- 找到所有指向3的引用点(也就是1和4),求他们的最近公共节点。
- 1的支配点是它自己,但是4的直接支配点并未处理(第一遍还没轮到它呢),所以4的支配点肯定是null。
- 这种情况下我们本次就先去掉父节点4,所以本次3的直接支配点是1。
- 继续往后求4的直接支配点,即2和3的最近公共节点,也就是1。
- 重头开始构建支配树,再次轮到3求直接支配点,因为4的直接支配点上一次已经求得是1,所以3最终的支配点就是1。
- 在构造过程中所有节点的支配点未改变,结束支配树的构造。
可以看到通过这种迭代的方式可以求得最终的构造树,但是缺点也显而易见,如果有向图过于复杂,时间复杂度会爆炸增长。其实构造支配树有一张更优的算法,Lengauer-Tarjan算法,不过我并没有去研究过,感兴趣的小伙伴可以去研究下。
3.4 Retained Size的计算
支配树构造完毕以后将每个对象的Shallow size加到各自的直接支配点上去就是某个对象的Retained Size大小。
上面就是HAHA库构造支配树和Reatined Size的整体流程。下面是代码实现:
//构建支配树并计算retainedSize
public void computeDominators() {
if (mDominators == null) {
//根据GC ROOT 计算拓扑序
mTopSort = TopologicalSort.compute(getGCRoots());
//根据拓扑序构建支配树
mDominators = new Dominators(this, mTopSort);
//根据支配树计算retained size
mDominators.computeRetainedSizes();
ShortestDistanceVisitor shortestDistanceVisitor = new ShortestDistanceVisitor();
shortestDistanceVisitor.doVisit(getGCRoots());
}
}
@NonNull
public static ImmutableList<Instance> compute(@NonNull Iterable<RootObj> roots) {
TopologicalSortVisitor visitor = new TopologicalSortVisitor();
visitor.doVisit(roots);
ImmutableList<Instance> instances = visitor.getOrderedInstances();
// We add the special sentinel node as the single root of the object graph, to ensure the
// dominator algorithm terminates when having to choose between two GC roots.
//设置一个超级源点
Snapshot.SENTINEL_ROOT.setTopologicalOrder(0);
// Set localIDs in the range 1..keys.size(). This simplifies the algorithm & data structures
// for dominator computation.
int currentIndex = 0;
//设置拓扑序号,为后面查找最近公共祖先做铺垫
for (Instance node : instances) {
node.setTopologicalOrder(++currentIndex);
}
return instances;
}
@Override
public void doVisit(Iterable<? extends Instance> startNodes) {
// root nodes are instances that share the same id as the node they point to.
// This means that we cannot mark them as visited here or they would be marking
// the actual root instance
// TODO RootObj should not be Instance objects
//将GC root压栈
for (Instance node : startNodes) {
node.accept(this);
}
//这里的算法是先根据右左根的方式放入队列
while (!mStack.isEmpty()) {
Instance node = mStack.peek();
//这里会把各自的实例对象加入栈中
//如果存在环,不会再进行入栈
if (mSeen.add(node.getId())) {
node.accept(this);
} else {
mStack.pop();
//因为可能存在环,所以这里是防止重复写入
if (mVisited.add(node.getId())) {
mPostorder.add(node);
}
}
}
}
//按照拓扑序反向去取出
ImmutableList<Instance> getOrderedInstances() {
return ImmutableList.copyOf(Lists.reverse(mPostorder));
}
/**
* Kicks off the computation of dominators and retained sizes.
*/
public void computeRetainedSizes() {
// Initialize retained sizes for all classes and objects, including unreachable ones.
for (Heap heap : mSnapshot.getHeaps()) {
for (Instance instance : Iterables.concat(heap.getClasses(), heap.getInstances())) {
//先重置变成shallow size
instance.resetRetainedSize();
}
}
//设置对象的支配点
computeDominators();
// We only update the retained sizes of objects in the dominator tree (i.e. reachable).
for (Instance node : mSnapshot.getReachableInstances()) {
int heapIndex = mSnapshot.getHeapIndex(node.getHeap());
// Add the size of the current node to the retained size of every dominator up to the
// root, in the same heap.
//计算某个对象的retained size就是从顶点开始到该对象的所有shallow size之和
for (Instance dom = node.getImmediateDominator(); dom != Snapshot.SENTINEL_ROOT;
dom = dom.getImmediateDominator()) {
dom.addRetainedSize(heapIndex, node.getSize());
}
}
}
//图中可能会存在环,所以有可能在处理某个点时父节点还未被处理,也就是没有支配点。
//当前的算法是如果父类没有支配点就先跳过,等父类的支配点计算完以后再迭代去计算,
//直到所有计算完所有的支配点即可。
//缺陷:如果图很复杂,话费的时间就会非常庞大。
private void computeDominators() {
// We need to iterate on the dominator computation because the graph may contain cycles.
// TODO: Check how long it takes to converge, and whether we need to place an upper bound.
boolean changed = true;
//有可能会存在环,进行迭代计算
while (changed) {
changed = false;
//把之前排好的拓扑序进行对象支配点的设置
for (int i = 0; i < mTopSort.size(); i++) {
Instance node = mTopSort.get(i);
// Root nodes and nodes immediately dominated by the SENTINEL_ROOT are skipped.
if (node.getImmediateDominator() != Snapshot.SENTINEL_ROOT) {
Instance dominator = null;
//如果有多个指向该对象的引用,就寻找最近支配点
for (int j = 0; j < node.getHardReferences().size(); j++) {
//拿到指向它的引用
Instance predecessor = node.getHardReferences().get(j);
if (predecessor.getImmediateDominator() == null) {
// If we don't have a dominator/approximation for predecessor, skip it
continue;
}
if (dominator == null) {
dominator = predecessor;
} else {
Instance fingerA = dominator;
Instance fingerB = predecessor;
//找到最近公共祖先,也就是最近支配点
while (fingerA != fingerB) {
//这里有个疑问,如果存在环的情况下,有可能拿出的支配点是null
//会有空指针?
if (fingerA.getTopologicalOrder() < fingerB.getTopologicalOrder()) {
fingerB = fingerB.getImmediateDominator();
} else {
fingerA = fingerA.getImmediateDominator();
}
}
dominator = fingerA;
}
}
//设置该对象的支配点
if (node.getImmediateDominator() != dominator) {
node.setImmediateDominator(dominator);
changed = true;
}
}
}
}
}
3.最短路径构建:
HAHA库对于引用链的构造也比较简单,从GC ROOT开始,利用PriorityQueue先进先出的性质,将构造一条从GC root开始不断加到实例变量上的引用链。下面是实现
@Override
public void doVisit(Iterable<? extends Instance> startNodes) {
// root nodes are instances that share the same id as the node they point to.
// This means that we cannot mark them as visited here or they would be marking
// the actual root instance
// TODO RootObj should not be Instance objects
//将GC root放入队列中
for (Instance node : startNodes) {
node.accept(this);
}
while (!mPriorityQueue.isEmpty()) {
Instance node = mPriorityQueue.poll();
mVisitDistance = node.getDistanceToGcRoot() + 1;
mPreviousInstance = node;
node.accept(this);
}
}
@Override
public void visitLater(Instance parent, @NonNull Instance child) {
//如果当前child不在队列中
//(是GC ROOT || 指向child的软引用列表不存在 || 指向chaild的软引用列表不包括当前的parent || child是一个软引用
if (mVisitDistance < child.getDistanceToGcRoot() &&
(parent == null ||
child.getSoftReferences() == null ||
!child.getSoftReferences().contains(parent) ||
child.getIsSoftReference())) {
child.setDistanceToGcRoot(mVisitDistance);
child.setNextInstanceToGcRoot(mPreviousInstance);
mPriorityQueue.add(child);
}
}