Android Camera 内存问题剖析,Android 屏幕适配很难嘛其实也就那么回事
[](
)问题
通过对 native 内存监控搜集到的日志进行堆栈聚合和 so 级的内存占用统计,可以发现截止到 OOM 时工具拦截到的 native 内存总量已经达到了 1.3G 左右(32 位下应用可直接使用的 native 内存上限约 2G),这其中占比最大的是 CameraMetaData 对象间接引用的内存,native 内存泄漏十分严重。
由于 native 内存分配的频率过高,获取 Java 层堆栈又比较耗时,在拦截 native 内存分配时并不适合直接频繁抓取 Java 堆栈。Native 内存不同于 Java 内存,单从拦截到的数据很难直观给出结论。通常对于内存等资源不合理使用导致的资源不足而引发的问题都很难归因,从拦截到的数据来看,CameraMetaData 所引用的内存最大,嫌疑也最大,基于此决定剖析一下这个问题
[](
)初步分析
[](
)分析 native 内存的分配和释放
通过拦截到的堆栈可以看出,CameraMetaData 的创建堆栈的上层是 Java 调用,最终在 native 层进行的内存分配(boot-framework.oat & libandroid_runtime.so)。CameraMetaData 对象有两部分内存,对象本身 & mBuffer 指向的 camera_metadata_t 所引用的内存;通过源码可知,每个 CameraMetadata 对象的 mBuffer 所指向的 camera_metadata_t 是独立的,彼此是不重叠的。
既然工具能拦截到这么多的未释放的内存分配,一定是因为这些内存的释放逻辑出问题导致的,我们需要优先调查清楚 CameraMetadata.mBuffer 的释放逻辑。通过分析 CameraMetadata.cpp 的源码可知,CameraMetadata::release()并未释放 mBuffer 所指向的内存,而是把 mBuffer 所指向的内存赋值给了另一个 CameraMetadata 对象;CameraMetadata::clear()是真释放,而 clear 的调用有两个场景:一个是在 camera_metadata_t 复用时,另一个是 CameraMetadat
a 对象析构时。
前述结论可知 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 方法执行超时导致的。
通过分析 finalizingReference = (FinalizerReference<?>)queue.remove() 发现这行代码后面的逻辑并没有对 ?finalizingReference 判空,说明这个地方一定不会返回空。既然不为空, queue.remove() 只能 block 等待,这个 ReferenceQueue.java 的源码也证实了猜想。
源码显示 goToSleep 是个同步方法,可能会 block。但遍历所有相关快照发现所有的 needToWork 属性均是 false,证明已经走过(只有 FinalizerWatchdogDaemon.INSTANCE.goToSleep() 会置为 false,而且这个函数是 private 的,只在 FinalizerDaemon 线程里调用),所以 block 在这里的可能性几乎没有。
其实 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 逻辑上!
评论