对内存问题的分析中一个必不可少的环节应该就是对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 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平台上常见的三种堆转储方法。
使用sdk自带的DDMS工具完成堆转储
使用Android Studio提供的Android Profiler工具完成堆转储
使用Android sdk提供的api堆转储,以下代码为api实例
1 2 3 4 5 6 7 8 9 10 11 12 try { String state = android.os.Environment.getExternalStorageState(); 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(); long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); removeWeaklyReachableReferences(); if (debuggerControl.isDebuggerAttached()) { return RETRY; } if (gone(reference)) { return DONE; } gcTrigger.runGc(); removeWeaklyReachableReferences(); if (!gone(reference)) { long startDumpHeap = System.nanoTime(); long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime); File heapDumpFile = heapDumper.dumpHeap(); if (heapDumpFile == RETRY_LATER) { return RETRY; } 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