LeakCanary

一、LeakCanary内存泄露检测

Java四大引用:

强引用:绝不回收
软引用:内存不足才回收
弱引用:碰到就回收
虚引用:等价于没有引用,只是用来标识下指向的对象是否被回收。

LeakCanary原理:

利用弱引用来指向Activity,并为它指定一个引用队列,然后在onDestroy()之后,去查看引用队列里是否有该Activity对应的弱引用,如果被放入,说明此activity已经被回收了;如果没被放入,则进行一次手动gc兜底,gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,弹窗提示给开发者。
参考:https://mp.weixin.qq.com/s/UfxG41HInNfv9nkDvKpcZQ

具体类:

在对象被销毁时通过WeakReference+ReferenceQueue检测对象是否被回收,延迟二次检测后还没被回收则认为是嫌疑对象,然后dump heap并对其进行分析…

详细流程:

  1. LeakCanary.install(application);此时使用application进行registerActivityLifecycleCallbacks,从而来监听Activity的何时被destroy。

  2. 在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收,检测方式如以下步骤。

  3. 使用一个弱引用WeakReference指向这个activity,并且给这个弱引用指定一个引用队列queue,同时创建一个key来标识该activity。

  4. 然后将检测的方法ensureGone()投递到空闲消息队列。

  5. 当空闲消息执行的时候,去检测queue里面是否存在刚刚的弱引用,如果存在,则说明此activity已经被回收,就移除对应的key,没有内存泄漏发生。

  6. 如果queue里不存在刚刚的弱引用,则手动进行一次gc。

  7. gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,否则没有发生内存泄漏,流程结束。

关键问题:

  1. Activity销毁了,如何监听onDestroy?
    通过Application注册Activity的生命周期回调,具体:通过application的registerActivityLifecycleCallbacks回调,在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收.

  2. 为何要进行一次手动gc?为什么要放入空闲消息里面去执行?
    Android的Gc过程是通过空闲消息实现的,优先级是很低。当MainLooper中没有消息执行时,就是空闲的,此时就会执行IdleHandlers里面的内容,gc才会得到执行。
    注意: activity 的onDestroy()被调用了,只是说明该activity被销毁了,并不是说已经发生了gc.
    我们的检测逻辑要放在gc之后,才能保证正确性,那就需要在mIdleHandlers执行之后了,但是,系统并没有提供比IdleHandlers优先级更低的工具,所以,我们也只能将我们的检测逻辑也放到IdleHandlers中;当检测逻辑运行时,大概率发生了gc,但也可能还没进行过gc,才能保证泄露结果的准确性。

  3. LeakCanary2发生了哪些变化?
    1)监控对象增加
    leakcanary2.6版本之前只能对Activity,Fragment进行监控。
    leakcanary2.6版本以后增加了对ViewModel,RootView,Service的监控。
    2)初始化方式
    leakcanary从2.0版本开始就不需要手动初始化了,其主要是通过ContentProvider来实现免初始化.
    参考:https://www.jianshu.com/p/2cba0ed5502d

LeakCanary 2 重写后的一些重要改变如下:
新的 heap 分析器,重新的实现并节省了 10 倍的内存(see Shark)
API 的更新用来简化配置,通过ContentProvider来实现免初始化.
使用了新的Heap分析工具Shark工具,放弃了之前的HAHA工具,新工具的内存比之前的要少90%,快6倍。
内部重写全采用 Kotlin
一次分析检测多个泄漏并按照泄漏的类型分组

  1. 为什么LeakCanary不能用于线上?
    直接将LeakCanary应用于线上会有如下一些问题:
    1.每次内存泄漏以后,都会生成一个.hprof文件,然后解析,并将结果写入.hprof.result。增加手机负担,引起手机卡顿等问题。
    2.多次调用GC,可能会对线上性能产生影响
    3.同样的泄漏问题,会重复生成 .hprof 文件,重复分析并写入磁盘。
    4…hprof文件较大,信息回捞成问题。

5.线上内存监控怎么处理?
快手的koom

6.LeakCanary怎么知道在onDestroy要监测的控件是哪些?
leakcanary2.6版本之前只能对Activity,Fragment进行监控。

leakcanary2.6版本以后增加了对ViewModel,RootView,Service的监控。

至于如何检测这些对象的销毁时机:

参考:https://www.jianshu.com/p/2cba0ed5502d

LeakCanary缺陷

在这里插入图片描述

二、ResourceCanary改进

ResourceCanary
微信对LeakCanary做了一些改造,将检测和分析分离,客户端只负责检测和dump内存镜像文件,文件裁剪后上报到服务端进行分析。
具体可以看这篇文章Matrix ResourceCanary – Activity 泄漏及Bitmap冗余检测:
https://mp.weixin.qq.com/s/XL55txToSCJXM8ErwrUGMw

作为 Matrix 的一个子模块,ResourceCanary 将把原本难以发现的 Activity 泄漏和重复创建的冗余 Bitmap 暴露出来,并提供引用链等信息帮助排查这些问题的根源,以提高微信客户端的代码质量。

细节改进

1.减少误报:对已判断为泄漏的Activity,记录其类名,避免重复提示该Activity已泄漏
2.裁剪 Hprof
3.提高 Hprof 分析效率

三、KOOM–线上内存泄漏监控

不管是LeakCanary 还是 ResourceCanary,他们都只能在线下使用,而线上内存泄漏监控方案,目前快手性能团队开源的KOOM的方案比较完善。

相对LeakCanary,KOOM的改进:

  1. 监控机制
    由LeakCanary的直接检查泄漏,改为:
    采用内存阈值检测方式(当内存使用率达到80%以上),
    周期性查询Java堆内存、线程数、文件描述符数等资源占用情况,
    将对象是否泄漏的判断延迟到了解析时,
    当连续多次超过设定阈值或突发性连续快速突破高阈值时,触发镜像采集,避免传统的频繁主动gc。

    1)间隔5s检测一次
    2)触发内存镜像采集的条件:
    当内存使用率达到80%以上
    两次检测时间内(例如5s内),内存使用率增加5%

  2. fork子进程,dump内存镜像–提高dump效率。
    镜像采集采用虚拟机supend->fork虚拟机进程->虚拟机resume->dump内存镜像的策略,将传统Dump冻结进程20s的时间缩减至20ms以内。
    我们知道LeakCanary检测内存泄漏,不能用于线上,是因为它dump内存镜像是在当前进程进行操作,会冻结App一段时间。
    所以,作为线上OOM监控,KOOM会fork子进程dump内存镜像
    1)fork成功以后,父进程立刻恢复虚拟机运行,解除冻结;
    2)主进程可以等待子进程dump结束,然后再返回执行内存镜像文件分析操作;
    3)并且子进程dump内存镜像期间不会受到父进程数据变动的影响。

  3. 解析Hprof文件流程
    解析性能优化: KOOM没有采用LeakCanary1.0版本的HAHA解析引擎,使用HAHA解析过程中非常容易OOM,且解析速度极慢。LeakCanary2.0版本使用Shark新版解析引擎;
    KOOM基于shark执行镜像解析,并针对shark做了一系列调整用于提升性能,在手机设备测即可执行离线内存泄露判定与引用链查找,生成分析报告。

总结:
KOOM利用Linux Copy-on-write机制fork子进程dump大大提高了dump效率。 内存阈值检测方式,将对象是否泄漏的判断延迟到了解析时,避免传统的频繁主动gc。

参考:官方文档

扩展

KOOM利用 Linux 的Copy-on-write机制(COW),fork子进程dump内存镜像
COW机制:写时复制

fork()会创建一个子进程,子进程的是父进程的副本;
exec()重新装载程序,清空数据;

一般的fork()会直接将父进程的数据拷贝到子进程中,拷贝完之后,会执行exec(),父进程和子进程之间的数据段和堆栈是相互独立的。

为了节省fork子进程的内存消耗和耗时,fork出的子进程并不会copy父进程的内存,而是和父进程共享内存空间,父子进程只在发生内存写入操作时,系统才会分配新的内存为写入方保留单独的拷贝。

进程保留了fork瞬间时父进程的内存镜像,且后续父进程对内存的修改不会影响子进程。

Copy-on-write的fork创建出的子进程,与父进程共享内存空间。既保留了镜像数据,同时子进程dump的过程也不会影响主进程执行**

暂停虚拟机需要调系统库,但谷歌从Android 7.0开始对调用系统库做了限制,基于此前提,快手自研了kwai-linker组件,绕过了这一限制

快手KOOM

总结

大厂OOM和监控方案
线上内存泄漏工具KOOM

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值