Android内存优化三:内存泄漏检测与监控

Android内存优化一:java垃圾回收机制
Android内存优化二:内存泄漏
Android内存优化三:内存泄漏检测与监控
Android内存优化四:OOM
Android内存优化五:Bitmap优化

Memory Profiler

Memory Profiler 是 Profiler 中的其中一个版块,Profiler 是 Android Studio 为我们提供的性能分析工具,使用 Profiler 能分析应用的 CPU、内存、网络以及电量的使用情况。

进入了 Memory Profiler 界面。

点击 Record 按钮后,Profiler 会为我们记录一段时间内的内存分配情况。

使用Memory Profiler 分析内存可以查看官网: 使用内存性能分析器查看应用的内存使用情况

Memory Analyzer Tool(MAT)

对于内存泄漏问题,Memory Profiler 只能提供一个简单的分析,不能够确认具体发生问题的地方。

而 MAT 就可以帮我们做到这一点,它是一款功能强大的 Java 堆内存分析工具,可以用于查找内存泄漏以及查看内存消耗情况。

  • 使用 Memory Profiler 的堆转储功能,导出 hprof(Heap Profile)文件。
  • as 生成hprof文件无法被mat识别,需要进行转换

    使用hprof-conv进行转换,hprof-conv位于sdk\platform-tools

    // 前一个为as生成的hprof文件,后一个为转换后的文件
    hprof-conv xxx.hprof xxx.hprof 
    

    ps:as导出hprof前最好先gc几次,可排除一些干扰

  • 使用mat打开转换后的文件
  • image

    Histogram 可以列出内存中的对象,对象的个数以及大小; Dominator Tree 可以列出那个线程,以及线程下面的那些对象占用的空间; Top consumers 通过图形列出最大的object; Leak Suspects 通过MA自动分析泄漏的原因。

  • Histogram
  • image

    Shallow Heap就是对象本身占用内存的大小,不包含其引用的对象内存,实际分析中作用不大。常规对象(非数组)的ShallowSize由其成员变量的数量和类型决定。数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定。对象成员都是些引用,真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],对象本身的内存都很小。

    Retained Heap值的计算方式是将Retained Set(当该对象被回收时那些将被GC回收的对象集合)中的所有对象大小叠加。或者说,因为X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。

    List objects -> with incoming references:查看这个对象持有的外部对象引用

    List objects -> with outcoming references:查看这个对象被哪些外部对象引用

  • OQL:对象查询语言
  • 使用对象查询语言可以快速定位发生泄漏的Activity及Fragment

    select * from instanceof android.app.Activity a where a.mDestroyed = true
    select * from instanceof androidx.fragment.app.Fragment a where a.mAdded = false
    

    LeakCanary

    使用 MAT 来分析内存问题,效率比较低,为了能迅速发现内存泄漏,Square 公司基于 MAT 开源了 LeakCanary,LeakCanary 是一个内存泄漏检测框架。

    集成LeakCanary后,可以在桌面看到 LeakCanary 用于分析内存泄漏的应用。

    当发生泄漏,会为我们生成一个泄漏信息概览页,可以看到泄漏引用链的详情。

    // 继承ContentProvider,在应用启动时,初始化LeakCanary
    internal sealed class AppWatcherInstaller : ContentProvider() {
      override fun onCreate(): Boolean {
            val application = context!!.applicationContext as Application
            InternalAppWatcher.install(application)
            return true
    
    internal class ActivityDestroyWatcher private constructor(
      private val objectWatcher: ObjectWatcher,
      private val configProvider: () -> Config
        // 在Activity执行onActivityDestroyed时,观察它的回收状态
      private val lifecycleCallbacks =
        object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
          override fun onActivityDestroyed(activity: Activity) {
            if (configProvider().watchActivities) {
              objectWatcher.watch(
                  activity, "${activity::class.java.name} received Activity#onDestroy() callback"
      companion object {
        // 通过application.registerActivityLifecycleCallbacks监听所有Activity的生命周期
        fun install(
          application: Application,
          objectWatcher: ObjectWatcher,
          configProvider: () -> Config
          val activityDestroyWatcher =
            ActivityDestroyWatcher(objectWatcher, configProvider)
          application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    
    // #ObjectWatcher
    // 在对象可达性发生更改时,垃圾收集器会将其插入到这个队列。
    private val queue = ReferenceQueue<Any>()
    // 受观察对象的缓存,保存受观察对象的弱引用
    private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
    // 1\. 观察对象
    @Synchronized fun watch(
        watchedObject: Any,
        description: String
        // 创建弱引用,watchedObject 为观察对象,即activity
        val reference =
          KeyedWeakReference(watchedObject,..., queue)
        // 保存受观察对象的弱引用
        watchedObjects[key] = reference
        checkRetainedExecutor.execute {
          moveToRetained(key)
    // 将可回收的对象从受观察对象的缓存中移除
    // 当对象变为弱可及(未被强引用),在最终确定或垃圾回收实际发生之前,会将WeakReferences入队
    private fun removeWeaklyReachableObjects() {
        var ref: KeyedWeakReference?
          ref = queue.poll() as KeyedWeakReference?
          // 从queue 取出的对象为弱可及,表示即将要回收的对象,即未发生泄漏情况
          // 所以,可以从受观察对象的缓存中移除它了
          if (ref != null) {
            watchedObjects.remove(ref.key)
        } while (ref != null)
    // 2\. 清理一下已回收的对象,如果对象已被回收,则无需再走下面的流程
    @Synchronized private fun moveToRetained(key: String) {
        removeWeaklyReachableObjects()
        // 如果已经被回收,则不会存在于缓存中
        val retainedRef = watchedObjects[key]
        if (retainedRef != null) {d
          onObjectRetainedListeners.forEach { it.onObjectRetained() }
    
    // #ObjectWatcher
    // 获取未被回收的对象数量
    val retainedObjectCount: Int
        @Synchronized get() {
            // 清理一下已回收的对象
          removeWeaklyReachableObjects()
          return watchedObjects.count { .. }
    # HeapDumpTrigger
    private fun checkRetainedObjects(reason: String) {
        val config = configProvider()
        // 3\. 获取未被回收的对象数量
        var retainedReferenceCount = objectWatcher.retainedObjectCount
        // 4\. 如果有对象未被回收,执行一次GC,然后再获取一次未被回收的对象数量
        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()
          retainedReferenceCount = objectWatcher.retainedObjectCount
            // 5\. 判断是否有泄漏,如果有,再判断是否需要提示
        if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
            // dump 对内存
        dumpHeap(retainedReferenceCount, retry = true)
    

    LeakCanary 会解析 hprof 文件,并且找出导致 GC 无法回收实例的引用链,这也就是泄漏踪迹(Leak Trace)。

    泄漏踪迹也叫最短强引用路径,这个路径是 GC Roots 到实例的路径。

    LeakCanary 存在几个问题,不同用于线上监控功能

  • 主动触发GC,会造成卡顿

  • Dump hprof,会造成app冻结

  • Hprof文件过大

  • 解析耗时过长

  • 解析本身有OOM风险

  • 线上监控需要做的,就是解决以上几个问题。

    各大厂都有开发线上监控方案,比如快手的KOOM,美团的Probe,字节的Liko

    快手自研OOM解决方案KOOM今日宣布开源

    总结一下几点:

  • 无主动触发GC不卡顿
  • 通过无性能损耗的内存阈值监控来触发镜像采集。将对象是否泄漏的判断延迟到了解析时

  • 高性能镜像DUMP
  • 利用系统内核COW(Copy-on-write,写时复制)机制,每次dump内存镜像前先暂停虚拟机,然后fork子进程来执行dump操作,父进程在fork成功后立刻恢复虚拟机运行,整个过程对于父进程来讲总耗时只有几毫秒,对用户完全没有影响。