字节跳动技术整理:Android-Camera 内存问题剖析,为什么 Flutter 能最好地改变移动开发

既然工具能拦截到这么多的未释放的内存分配,一定是因为这些内存的释放逻辑出问题导致的,我们需要优先调查清楚 CameraMetadata.mBuffer 的释放逻辑。通过分析 CameraMetadata.cpp 的源码可知,CameraMetadata::release()并未释放 mBuffer 所指向的内存,而是把 mBuffer 所指向的内存赋值给了另一个 CameraMetadata 对象;CameraMetadata::clear()是真释放,而 clear 的调用有两个场景:一个是在 camera_metadata_t 复用时,另一个是 CameraMetadata 对象析构时。

前述结论可知 CameraMetadata.mBuffer 所指向的 camera_metadata_t 是彼此独立的。通过工具拦截到的堆栈和分配数量猜测,Native OOM 时内存中一定存在大量的 CameraMetadata 实例。C++对象的析构通常是调用 delete 来实现的,AOSP 里想搜索哪里 delete 了一个 CameraMetaData 对象是很难的,因为很难知道 delete 时的变量名。根据一个基本的 C++编程规范,内存通常在哪里创建的,应该就在那里释放,我们全局搜索 new CameraMetaData 字符串就可以很轻松的发现 CameraMetaData 对象的创建和释放均是在/frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp 里实现的。



通过 android_hardware_camera2_CameraMetadata.cpp 里的注册清单可以看到与这些函数关联的 Java 层 class 是 android/hardware/camera2/impl/CameraMetadataNative,CameraMetadata_close 函数在 Java 对应的是 nativeClose 函数。可以进一步发现 CameraMetaDataNative 里 nativeClose 函数是在 close 函数里调用的,而 close 函数又是在 finalize 函数调用的。


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

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

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

通过分析 FinalizerDaemon 的源码猜测还有另外一种可能,就是该线程的核心逻辑可能 block 在某个同步逻辑上,根据判有两处代码有可能:一个是 FinalizerWatchdogDaemon.INSTANCE.goToSleep()?另一个是 finalizingReference = (FinalizerReference<?>)queue.remove()

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

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

其实 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 逻辑上!


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

综上,这类 native OOM 的根本原因是:当应用自身的 native 内存本身已处于高水位时,开启相机后,相机服务会持续通过 binder 通信在应用侧创建 CameraMetadataNative 对象,创建 CameraMetadataNative 对象的同时也会在应用侧通过 jni 接口在 native 层创建/复用一块存放 camera_metadata_t 的相对比较大的内存。由于 Java 层的 CameraMetadataNative 对象本身比较小,这种连续创建小对象的行为一定时间内很难触发 Java 层的 GC,导致其间接引用的 native 内存不断上涨,最终触发虚拟内存上限而 crash。
解决思路
问题的原因虽然相对比较简单,但如何解决这类问题还是比较难抉择的。既然是 GC 不及时导致的,一种简单的方案就是在拍摄页周期性触发 GC。但如果 GC 间隔比较小,GC 毕竟是耗时的,GC 过于频繁会严重影响拍摄体验;如果 GC 间隔时间比较长,还是会有大概率重蹈这类 native OOM 的覆辙。
主动触发 GC 的方案很难平衡对性能的影响。其实问题的重点不是 Java 层,而是 Java 对象引用的 native 内存,如果及时主动释放这部分内存就可以从根本上彻底解决此类问题。通过前面的分析可以知道,这部分内存原本是在 GC 时的 finalize 环节回收,但如果提前发现 CameraMetadataNative 不再使用时,主动触发来释放这部分内存就可以一劳永逸。通过分析源码可以发现 CameraMetadataNative 传递到应用层之后后续并未再使用,在应用层使用完 CameraMetadataNative 对象之后,通过反射调用 close 函数即可释放其所引用的 native 内存。

评论