排查 Java 堆现场
幸运的是我们通过内存快照裁剪工具(Tailor)轻松拿到了大量这类 native OOM 时对应的 Java 堆内存快照文件 。这些内存快照文件完美证实了之前的猜想,当发生这类 native OOM 时 Java 层的确存在大量的 CameraMetadataNative 对象 。以下图为例,这些 CameraMetadataNative 对象里除 6 个被其他代码引用外,其余对象全部在 FinalizerDaemon 线程的队列里,等待执行 finalize 方法 。同时,快照里有 6658 个对象,只有大约 600+对象的 mMetadataPtr 是等于 0 的,说明这部分对象对应的 Native 内存需要在 finalize 时释放,这跟工具拦截的数据是完全匹配的,也间接验证了 Native 内存监控的正确性和可靠性
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H5MG-9.jpg)
文章插图
深入分析排查 Finalize 执行
虽然上述分析验证了问题,也证实了之前的猜想,但仍未找到导致此类问题的深层次原因,对于最终解决此类问题也仍然束手无策 。为什么会有这么多的 CameraMetadataNative 对象等待执行 finalize 方法或许是下一步的调查方向 。做过 Java 稳定性治理的同学应该都知道一类很有名的 TimeoutException 异常,这类异常的根本原因是 finalize 执行超时导致的,这个 case 会不会是某个对象的 finalize 执行超时导致的?
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H51P0-10.jpg)
文章插图
【Android Camera 内存问题剖析】
结合 FinalizerDaemon 的源码可以看到,每执行一个对象的 finalize 方法时,都会通过finalizingObject属性记录当前的对象 。如果真的是 finalize 超时导致的,一定存在 finalizingObject 属性不为空的现场 。我们在遍历完所有相关内存快照里的 FinalizerDaemon 线程状态后发现,这些现场的 finalizingObject 属性均为空 。这个结果很意外,似乎并不是某个对象的 finalize 方法执行超时导致的 。
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H51053-11.jpg)
文章插图
通过分析finalizingReference = (FinalizerReference<?>)queue.remove() 发现这行代码后面的逻辑并没有对 finalizingReference 判空,说明这个地方一定不会返回空 。既然不为空, queue.remove() 只能 block 等待,这个 ReferenceQueue.java 的源码也证实了猜想 。
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H52934-12.jpg)
文章插图
源码显示 goToSleep 是个同步方法,可能会 block 。但遍历所有相关快照发现所有的 needToWork 属性均是 false,证明已经走过(只有FinalizerWatchdogDaemon.INSTANCE.goToSleep() 会置为 false,而且这个函数是 private 的,只在 FinalizerDaemon 线程里调用),所以 block 在这里的可能性几乎没有 。
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H5IU-13.jpg)
文章插图
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H53R2-14.jpg)
文章插图
其实 block 在这里的原因通常是因为只有在 GC 时才会将需要执行 finalize 的对象加入到 FinalizerDaemon 的队列里 。如果一段时间内没有 GC,且队列就为空时,上面的 remove 会一直 block,直到 GC 后才有对象加入到这个队列里 。巧合的是我们在发生这类 native OOM 时会通过 Tailor 主动 dump Java 堆的内存快照,而 dump 快照时会触发 GC & suspend,这个最终导致大量的 CameraMetadataNative 对象被同时加入到 FinalizerDaemon.queue 的队列里 。
分析 GC 策略
通过上述分析可知如果不是 GC,这些对象是不会被被加入到 FinalizerDaemon.queue 里的,这说明这类 native OOM 发生前的一段时间内一直没有 GC,才导致大量 CameraMetadataNative 对象没有及时执行 finalize,进而发生 native OOM 。以上分析也在线下进入到拍摄页后静置观察实验中得到验证,这其中大概每隔 30s-40s 甚至更长时间 Java 堆才会主动触发一次 GC,在这期间 native 内存会不断增长,直到 GC 后才会大幅下降,Java & Native 内存才会恢复到正常水平 。虽然问题不是 block 在 finalize 环节,但最终这个问题的原因被锁定在了 GC 逻辑上!
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H5NK-15.jpg)
文章插图
![Android Camera 内存问题剖析](http://img.jiangsulong.com/220421/045H55S9-16.jpg)
文章插图
了解 GC 的同学可能会知道 ART 虚拟机的 GC cause 有很多种,kGcCauseForAlloc/kGcCauseBackground 是虚拟机最易频繁触发的 。当停留在拍摄页不做任何操作时,程序逻辑相对简单,这期间只有相机服务周期(>=30 次/s)地通过 binder 在应用端触发创建 CameraMetadataNative 对象,并在拍摄页显示一张相机采集到的图像 。这个过程 Java 堆只有 CameraMetadataNative 对象创建,而 CameraMetadataNative 自身占用内存比较小,一次 GC 之后 Java 堆内存比较富裕的情况下,虚拟机很长一段时间内不会主动触发 GC 。如果这期间 native 内存的增幅过大,在下次 GC 之前触顶就发生 native OOM
推荐阅读
- 铠侠 极至光速系列内存卡评测:经典红白复刻,唯有品质依旧
- Android Jetpack架构组件Navigation管理Fragment框架
- 多进程编程 - 共享内存
- IDE 最佳Android应用程序开发工具
- 虚拟内存 和 page fault 的解释
- 对于内存结构的简单理解
- 电脑卡慢时升级内存是最低级做法,正确顺序是这样
- 高频内存对性能影响到底有几何?试试看就知道了
- Android WebView 优化梳理
- 定位Flutter内存问题很难么?