不冻结 APP 的 dump hprof

LeakCanary 浅析Matrix - ResourcesCanary 浅析 这两篇文章里,介绍了检测内存泄漏的两种相似的思路:

  1. WeakReference + ReferenceQueue + 延迟 5s 检查是否被 GC
  2. WeakReference + ConcurrentLinkedQueue,子线程轮询的方式每隔 1m 检查队列里的对象是否被 GC

但无论 LeakCanary 还是 Matrix.ResourcesCanary 都没有解决 dump hprof 时整个 APP 被「冻结」的问题,而一次 dump hprof 往往要持续 10s 甚至更多,这在线上环境下是不可接受的,所以在发生内存泄漏后只能上报类信息,开发者收到后人工检查涉及此类的相关代码找出泄漏点。但是这种方式是极其低效和不准确的,如果能在端侧找出泄漏对象的 GC ROOT PATH,就能极大地减少人工量,并且提高后续修复的准确度。

如果能够在不冻结 APP 的情况下 dump hprof,不仅仅能够上报内存泄漏问题,还能对 OOM 进行预警:比如监控 JVM 的内存使用率,当达到 90% 的时候将 hprof 上报分析

KOOM 提出了一个在不冻结 APP 的情况下 dump hprof 的思路:fork 出子进程,总体流程是这样的:

  1. 父进程 suspend JVM
  2. 父进程 fork 出子进程
  3. 父进程 resume JVM 后,线程等待子进程结束从而拿到 hprof
  4. 子进程调用 Debug.dumpHprofData 生成 hprof 后退出
  5. 父进程启动 Service 在另一个进程里解析 hprof 并构造 GC ROOT PATH

整个过程 APP 只在 fork 前后冻结了一小会,这么短的时间是可以接受的,由于 fork 采用 Copy-On-Write 机制,子进程能够继承父进程的内存

暂停和恢复 JVM

  1. <= Android 10,使用函数 SuspendVMEvResumeVMEv
  2. \>= Android 11,使用类 ScopedSuspendAll(局部变量,构造函数暂停 JVM,析构函数恢复 JVM)
  3. <= Android 4,KOOM 不支持

但是要从各个 Android 版本的 so 文件里准确找到这些函数(符号)是有难度的,寻找 SuspendVMEvResumeVMEv 的地址代码如下