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必须的数据。裁剪还有数据脱敏的好处,只保留对分析问题有用的内存中类与对象的组织结构,并不上传真实的业务数据,充分保护用户隐私。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容