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

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

Memory Profiler

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

进入了 Memory Profiler 界面。

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

image

在内存分配面板中,通过拖动时间线来查看一段时间内的内存分配情况

通过搜索类或者报名的方式查看对象的使用情况

image

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

Memory Analyzer Tool(MAT)

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

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

  1. 使用 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几次,可排除一些干扰

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

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

  1. Histogram
image

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

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

  1. 引用链
image

Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用。从GC上说,除了强引用外,其他的引用在JVM需要的情况下是都可以 被GC掉的,如果一个对象始终无法被GC,就是因为强引用的存在,从而导致在GC的过程中一直得不到回收,因此就内存泄漏了。

image

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

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

  1. 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
image

LeakCanary

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

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

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

image

初始化

// 继承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?
    do {
      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

KOOM

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

总结一下几点:

  • 无主动触发GC不卡顿

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

  • 高性能镜像DUMP

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

  • hprof分析于裁剪

  • 采用边缘计算的思路,将内存镜像于闲时进行独立进程单线程本地分析,不过多占用系统运行时资源;分析完即删除,不占用磁盘空间;分析报告大小只有KB级别,不浪费用户流量。

  • 针对镜像回捞需求,对hprof进行运行时hook裁剪,只保留分析OOM必须的数据。裁剪还有数据脱敏的好处,只保留对分析问题有用的内存中类与对象的组织结构,并不上传真实的业务数据,充分保护用户隐私。

推荐阅读更多精彩内容