Android Camera 内存问题剖析( 二 )


排查 Java 堆现场
幸运的是我们通过内存快照裁剪工具(Tailor)轻松拿到了大量这类 native OOM 时对应的 Java 堆内存快照文件 。这些内存快照文件完美证实了之前的猜想,当发生这类 native OOM 时 Java 层的确存在大量的 CameraMetadataNative 对象 。以下图为例,这些 CameraMetadataNative 对象里除 6 个被其他代码引用外,其余对象全部在 FinalizerDaemon 线程的队列里,等待执行 finalize 方法 。同时,快照里有 6658 个对象,只有大约 600+对象的 mMetadataPtr 是等于 0 的,说明这部分对象对应的 Native 内存需要在 finalize 时释放,这跟工具拦截的数据是完全匹配的,也间接验证了 Native 内存监控的正确性和可靠性

Android Camera 内存问题剖析

文章插图
 
深入分析排查 Finalize 执行
虽然上述分析验证了问题,也证实了之前的猜想,但仍未找到导致此类问题的深层次原因,对于最终解决此类问题也仍然束手无策 。为什么会有这么多的 CameraMetadataNative 对象等待执行 finalize 方法或许是下一步的调查方向 。做过 Java 稳定性治理的同学应该都知道一类很有名的 TimeoutException 异常,这类异常的根本原因是 finalize 执行超时导致的,这个 case 会不会是某个对象的 finalize 执行超时导致的?
Android Camera 内存问题剖析

文章插图
【Android Camera 内存问题剖析】 
结合 FinalizerDaemon 的源码可以看到,每执行一个对象的 finalize 方法时,都会通过finalizingObject属性记录当前的对象 。如果真的是 finalize 超时导致的,一定存在 finalizingObject 属性不为空的现场 。我们在遍历完所有相关内存快照里的 FinalizerDaemon 线程状态后发现,这些现场的 finalizingObject 属性均为空 。这个结果很意外,似乎并不是某个对象的 finalize 方法执行超时导致的 。
Android Camera 内存问题剖析

文章插图
 
通过分析finalizingReference = (FinalizerReference<?>)queue.remove() 发现这行代码后面的逻辑并没有对 finalizingReference 判空,说明这个地方一定不会返回空 。既然不为空, queue.remove() 只能 block 等待,这个 ReferenceQueue.java 的源码也证实了猜想 。
Android Camera 内存问题剖析

文章插图
 
源码显示 goToSleep 是个同步方法,可能会 block 。但遍历所有相关快照发现所有的 needToWork 属性均是 false,证明已经走过(只有FinalizerWatchdogDaemon.INSTANCE.goToSleep() 会置为 false,而且这个函数是 private 的,只在 FinalizerDaemon 线程里调用),所以 block 在这里的可能性几乎没有 。
Android Camera 内存问题剖析

文章插图
 

Android Camera 内存问题剖析

文章插图
 
其实 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 内存问题剖析

文章插图
 

Android Camera 内存问题剖析

文章插图
 
了解 GC 的同学可能会知道 ART 虚拟机的 GC cause 有很多种,kGcCauseForAlloc/kGcCauseBackground 是虚拟机最易频繁触发的 。当停留在拍摄页不做任何操作时,程序逻辑相对简单,这期间只有相机服务周期(>=30 次/s)地通过 binder 在应用端触发创建 CameraMetadataNative 对象,并在拍摄页显示一张相机采集到的图像 。这个过程 Java 堆只有 CameraMetadataNative 对象创建,而 CameraMetadataNative 自身占用内存比较小,一次 GC 之后 Java 堆内存比较富裕的情况下,虚拟机很长一段时间内不会主动触发 GC 。如果这期间 native 内存的增幅过大,在下次 GC 之前触顶就发生 native OOM


推荐阅读