Android内存分析的一般方法

对内存问题的分析中一个必不可少的环节应该就是对hprof的分析了,常见的MAT和Leakcanry都是针对hprof文件的分析工具。这篇文章就记录下做内存分析的一般工具和分析步骤。

使用dumpsys分析内存

adb shell dumpsys meminfo $applicationId [-d]

私有内存 Private (Clean and Dirty)
这是仅属于你的进程的内存,这是你的进程被销毁时系统可以回收的RAM。通常情况下最重要的部分是Private Dirty RAM。

按比例分配占用内存 PSS
这表示您的应用的 RAM 使用情况,考虑了在各进程之间共享 RAM 页的情况。您的进程独有的任何 RAM 页会直接影响其 PSS 值,而与其他进程共享的 RAM 页仅影响与共享量成比例的 PSS 值。例如,两个进程之间共享的 RAM 页会将其一半的大小贡献给每个进程的 PSS。

通常情况下,仅需关注 Pss Total 和 Private Dirty 列。一些情况下,Private Clean 和 Heap Alloc 列提供的数据也需要关注。

Dalvik Heap

PSS Total: 包括所有 Zygote 按比例分配的内存。
Private Dirty: 数值是仅分配到您应用的堆的实际 RAM,是由自己的分配和任何 Zygote 分配页组成,这些分配页自从 Zygote 派生应用进程以来已被修改(COW)。

.so mmap 和 .dex mmap

Pss Total: 包括应用之间共享的平台代码
Private Clean: 是应用自己的代码

.oat mmap

这是代码映像占用的 RAM 量,根据多个应用通常使用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响

EGL mtrack 和 GL mtrack

EGL mtrack: gralloc分配的内存,主要是窗口系统,SurfaceView/TextureView和其他的由gralloc分配的GraphicBuffer总和
GL mtrack: 驱动上报的GL内存使用情况。 主要是GL texture大小,GL command buffer,固定的全局驱动程序RAM开销等的总和

这里有一个小技巧,应用开发者调用startTrimMemory会帮助app或者系统更多的释放内存,减少内存压力,但是调用的位置和时机要慎重,因为清除了缓存,在下一次绘制(vsync的下一个信号到来)的时候绘制效率不会很高。详细原理可参考:https://cloud.tencent.com/developer/article/1070616

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 清除GPU绘图缓存
*/
private void cleanGraphicsCache() {
Object instance = ReflectUtils.getStaticMethod("android.view.WindowManagerGlobal",
"getInstance", null, null);

try {
Class threadClazz = Class.forName("android.view.WindowManagerGlobal");
Method m1 = threadClazz.getDeclaredMethod("trimMemory", int.class);
m1.invoke(instance, TRIM_MEMORY_COMPLETE);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

TOATAL

进程占用的按比例分配占用内存 (PSS) 总量。等于上方所有 PSS 字段的总和。表示您的进程占用的内存量占整体内存的比重,可以直接与其他进程和可用总 RAM 比较。
Private Dirty 和 Private Clean 是进程中的总分配,未与其他进程共享。它们(尤其是 Private Dirty)等于你的进程被破坏后将释放回系统中的 RAM 量。Dirty RAM 是因为已被修改而必须保持在 RAM 中的 RAM 页(因为没有交换);Clean RAM 是已从某个持久性文件(例如正在执行的代码)映射的 RAM 页,如果一段时间不用,可以移出分页。

ViewRootImpl
根视图数量,每一个根视图关联一个window,由此可确定涉及对话框或其他window的内存泄露。

AppContexts 和 Activities
Context和Activity对象数量,用于快速确定Activity的泄露情况。

0x03 使用堆转储工具

将android app的内存信息转换成hprof格式的磁盘文件,这个过程就叫堆转储。堆转储的目标当然是为了获取当前Java虚拟机的内存信息以供排查内存问题。在android平台上常见的三种堆转储方法。

  1. 使用sdk自带的DDMS工具完成堆转储
  2. 使用Android Studio提供的Android Profiler工具完成堆转储
  3. 使用Android sdk提供的api堆转储,以下代码为api实例
1
2
3
4
5
6
7
8
9
10
11
12
try {
String state = android.os.Environment.getExternalStorageState();
// 判断SdCard是否存在并且是可用的
if (android.os.Environment.MEDIA_MOUNTED.equals(state)) {
String dumpDir = Hprof.getDumpDir(context);
android.os.Debug.dumpHprofData(dumpDir);
Logger.d(String.format("create dumpfile %s done!", dumpDir));
return dumpDir;
}
} catch (IOException e) {
e.printStackTrace();
}

0x04 使用hprof文件分析工具

Memory Analyzer Tool(简称MAT) 使用方法可参考: Android内存优化之二:MAT使用进阶
这里补充的是,MAT支持使用OQL(对象查询语言)查询堆文件信息,你可以像类似使用SQL查询数据库一样查询堆文件上的内存对象的信息,举个例子,我想知道当前内存中有多少个Activity?

1
select * from instanceof android.app.Activity

查询大小大于100kb大小的Bitmap对象,图略

1
select * from instanceof android.graphics.Bitmap s where s.@retainedHeapSize >100000

关于OQL详情参考:http://help.eclipse.org/kepler/index.jsp,搜索OQL

0x05 使用Leakcanary监控泄露

LeakCanary是内存泄露检测工具,LeakCanary 中文使用说明和LeakCanary的开源地址:https://github.com/square/leakcanary

如何监控内存泄露的发生?
利用弱引用。当JVM进行垃圾回收时,无论内存是否充足,如果该对象只有弱引用存在,那么该对象会被垃圾回收器回收。所以Leakcanary在进行内存泄露的监控时,利用弱引用的上述特性,在对象生命周期结束后主动gc并检查该对象的弱引用是否被回收,如果弱引用没有被正常回收,说明在对象生命周期结束以后,该对象还被其他对象持有他的非弱引用。该对象还有到达GC ROOTS的可达路径。如果在对象生命周期结束后弱引用不存在了,说明该对象已经被JVM的垃圾回收器正常回收了,该对象的内存空间也被正常回收。所以Leakcanary设计成是对有明确生命周期的对象的自动监控,比如Activity对象,也可以是你想要跟踪的有明确生命周期的对象。

如何判断弱引用被回收?
利用ReferenceQueue。当垃圾回收器准备回收一个被引用包装的对象时,该引用会被加入到关联的ReferenceQueue。程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。

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
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
//计算watch方法到gc垃圾回收的时长
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//尝试移除已经到达引用队列的弱引用
removeWeaklyReachableReferences();
//判断是否在debug
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks. (debug可以创造错误的内存泄露)
return RETRY;
}
if (gone(reference)) {//若当前对象已经可达了,即不会造成你内存泄露
return DONE;
}
//手动gc,确保引用对象是否真的被回收了。因为在dump内存信息之前提示内存泄露的时候,希望系统经过充分gc垃圾回收,而不存在任何的误判,对leakcanary容错性的考虑
gcTrigger.runGc();
//清除已经到达引用队列的弱引用
removeWeaklyReachableReferences();
if (!gone(reference)) {//此时对象还没到达对列,代表已经内存泄露了
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
//dump出内存泄露的heap文件,这里可能触发GC
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.(不能dump heap堆文件)
return RETRY;
}
//dump heap文件的时间计算
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
//真正分析内存泄露以及路径
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
heapdumpListener.analyze(heapDump);
}
return DONE;
}

如何分析对象的被引用情况?
遍历对象树。

0x06 使用三方库手动分析hprof文件

可使用square提供的堆文件分析库(com.squareup.haha:haha:2.0.4)对hprof文件进行解析得到对象树。

1
2
3
4
5
6
7
8
9
10
11
12
13
private Snapshot getSnapShot(String hprofPath) {
File heapDumpFile = new File(hprofPath);
HprofBuffer hprofBuffer = null;
try {
hprofBuffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(hprofBuffer);
return parser.parse();
} catch (IOException e) {
e.printStackTrace();
}

return null;
}

6.1 对象搜索

可根据解析获得的Snapshot对象完成对特定类名对象的查找操作。进而批量分析出当前内存中执行类型对象的数量和对象引用情况。

1
2
3
4
5
6
7
8
List<ClassObj> classObjList = snapshot.findAllDescendantClasses(filterKey());
List<Instance> allInstance = new ArrayList<>();
for (ClassObj classObj : classObjList) {
ClassObj instanceClass = snapshot.findClass(classObj.getClassName());
for (Instance instance : instanceClass.getInstancesList()) {
allInstance.add(instance);
}
}

6.2 对象树遍历

可通过解析到的snapshot对象获取Root节点,自定义对象数爬虫,从Root节点遍历整个对象树,查找出异常对象的分布。如希望查找出当前对象树中,找到最大的组件类型有哪些:

1
2
3
4
mDistanceVisitor = new RetainedSizeVisitor();
snapshot.computeDominators();
// 找到最大的组件类型
mDistanceVisitor.doVisit(snapshot.getGCRoots());
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
/**
* @author Ivonhoe on 2018/9/26.
*/
public class RetainedSizeVisitor extends NonRecursiveVisitor {

/**
* App space中retained size最大的对象
*/
private FixedSizePriorityQueue mMaxRetainedQueue;

private List<HeapInstanceEntry> maxInstance;

public RetainedSizeVisitor() {
mMaxRetainedQueue = new FixedSizePriorityQueue(30);
maxInstance = new ArrayList<>();
}

@Override
public void visitRootObj(RootObj root) {
super.visitRootObj(root);
}

@Override
protected void defaultAction(Instance instance) {
super.defaultAction(instance);

findTopNRetainedSizeInstanceInAppSpace(instance);
}

/**
* 找到retained大小最大的n个对象
*/
private void findTopNRetainedSizeInstanceInAppSpace(Instance child) {
if (child != null && !(child instanceof ClassObj) && child.getHeap().getId() == 'A') {
mMaxRetainedQueue.add(child);
}
}

@Override
public void doVisit(Iterable<? extends Instance> startNodes) {
super.doVisit(startNodes);
}

private static class FixedSizePriorityQueue {
private PriorityQueue<Instance> queue;
private int maxSize; //堆的最大容量

public FixedSizePriorityQueue(int maxSize) {
if (maxSize <= 0) throw new IllegalArgumentException();
this.maxSize = maxSize;
this.queue = new PriorityQueue(maxSize, new Comparator<Instance>() {
@Override
public int compare(Instance o1, Instance o2) {
return ((Long) o1.getTotalRetainedSize()).compareTo(o2.getTotalRetainedSize());
}
});
}

public void add(Instance e) {
if (queue.size() < maxSize) { //未达到最大容量,直接添加
queue.add(e);
} else { //队列已满
Instance peek = queue.peek();
if (peek != null && (e.getTotalRetainedSize() > peek.getTotalRetainedSize())) {
queue.poll();
queue.add(e);
}
}
}
}
}

参考文档

https://developer.android.com/studio/command-line/dumpsys#meminfo
https://cloud.tencent.com/developer/article/1070616
https://cloud.tencent.com/developer/article/1070616
http://gityuan.com/2016/01/02/memory-analysis-command/
https://blog.csdn.net/msf568834002/article/details/78881341