一、写在本篇之前
程序员是一份辛苦的工作,之前我总想着每一篇写很多内容,后来感觉也不能操之过急,AOSP的源码过于庞大,全部看完不太现实,只能抓住重点,在源码中学习大牛们的设计技巧和流程精髓。
因此,后续我的更新将尽可能每次只写一个主题,也少让自己熬夜。但是与此同时,为了避免单调,我将会在文章中穿插一些自己对性能优化工作的感悟。
二、perfetto内存分析
我们还是看前面捞取到的perfetto trace来看内存。事实上,perfetto支持抓取单个进程的堆栈,既可以抓取java堆内存,也可以抓取native的堆内存,但是后者的条件实际调试会更严苛一点,
这实际上是一个比较基础的操作,他的意义在于可以帮你清晰展现内存堆栈调用的关系,定位OOM内存泄露问题。这里有一篇可供参考的使用perfetto脚本抓取java堆栈的参考链接:https://blog.csdn.net/gitblog_00033/article/details/154714076。
这里的核心主要是使用脚本抓取,以及使用perfetto网站分析。图片来自上面文章,自己抓效果类似。
这幅图片就是一个Java堆栈的实例,可以清晰看到调用栈和每个class或者方法占用的大小。
同时也可以使用抓取的整个性能trace看内存,如下实例,不同的trace配置会有不同的呈现,这里摘取的网上找的trace看的是RSS,这个有些偏大,绝大多数基本都只呈现java heap的大小。
抓取内存数据一般比较常用的是dumpmeminfo 和dumpshowmaps。安卓中的指令一般为:
# 监控堆内存使用
adb shell dumpsys meminfo <package_name>
使用该命令抓取的各个段的内存和大致解析如下图:
这里有一篇文章对meminfo的解析比较深入:https://www.cnblogs.com/Linux-tech/p/12961295.html。
核心的点是,dumpmeminfo的底层原理还是binder调用,通过binder调用访问进程的虚拟机,从虚拟机中获得内存的值。
我们仅将dumpmeminfo作为一种工具。
我们实际工作中会遇到很多应用导致的OOM问题,很多同学会看log发现有时候有些程序在内存接近400MB时候OOM,随后crash,重启进程,有些在500多MB时候才oom。什么是OOM,out of memory,内存泄露。
很多同学会有疑问,OOM时虚拟机查杀的依据是什么?
dalvik.vm.heapgrowthlimit=256m // 虚拟机分配给APP的初始空间大小
dalvik.vm.heapstartsize=64m
dalvik.vm.heapsize=512m //这个值最重要,一般情况下,这个值在虚拟机中默认是384MB,也就是说应用不能超过这个值,否则会OOM,配置为512MB也只是对极少数系统自己豁免的应用生效。
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=1m
dalvik.vm.heapmaxfree=8m
上面这些虚拟机配置参数就是OOM查杀时候的依据。更详细的参考这个网站文章 https://article.juejin.cn/post/7479350849056882740,上述参数的配置优化策略见下图,我这里仅关注heapsize。
Java Heap 的 OOM 触发逻辑
1. 当Committed(已提交内存)接近Max(堆上限)时:
2. VM 会优先触发GC(垃圾回收),释放无用对象的 Allocated 内存,转化为 Free;
3. 若 GC 后 Free 仍不足,且 Committed 已达 Max,则抛出OutOfMemoryError;
这也是为什么调整dalvik.vm.heapsize(Max 值)能临时缓解 OOM,但不是根本解决方案。
关于这个配置的AOSP源码路径和关键逻辑如下:
Android 16 中已无原生 Dalvik,堆大小设置逻辑完全迁移到 ART 的art/runtime/heap_options.cc(核心解析)和art/runtime/heap.cc(初始化,heap.cc和heap.h定义了堆内存的基本结构和操作。
class Heap {
public:
// 年轻代空间,包含Eden和Survivor区
Space* young_space_;
// 老年代空间
Space* old_space_;
// 大对象空间
Space* large_object_space_;
//...其他空间和属性
}; 参考 https://blog.csdn.net/qq_28540861/article/details/148267563
如下是heap.h的源码和heap.cc的源码
CollectGarbageInternal最后才是跟上层真正的交互接口,看到的startgc只不是是其中的后置步骤。
// 调用收集器的Run():最终执行PerformGc()(标记存活对象→清除死对象→压缩内存)
collector->Run(gc_cause, clear_soft_references || runtime->IsZygote());
如果需要gc,会调用 RequestConcurrentGCAndSaveObject 方法,进而调用 RequestConcurrentGC,最后仍然调用了CollectGarbageInternal 进行gc,所以
CollectGarbageInternal 也是 ART虚拟机里垃圾回收的入口。这里这篇文章讲的比较深入,建议参考https://blog.csdn.net/chuyouyinghe/article/details/142652024。