LeakCanary
一、LeakCanary内存泄露检测
Java四大引用:
强引用:绝不回收
软引用:内存不足才回收
弱引用:碰到就回收
虚引用:等价于没有引用,只是用来标识下指向的对象是否被回收。
LeakCanary原理:
利用弱引用来指向Activity,并为它指定一个引用队列,然后在onDestroy()之后,去查看引用队列里是否有该Activity对应的弱引用,如果被放入,说明此activity已经被回收了;如果没被放入,则进行一次手动gc兜底,gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,弹窗提示给开发者。
参考:https://mp.weixin.qq.com/s/UfxG41HInNfv9nkDvKpcZQ
具体类:
在对象被销毁时通过WeakReference+ReferenceQueue检测对象是否被回收,延迟二次检测后还没被回收则认为是嫌疑对象,然后dump heap并对其进行分析…
详细流程:
-
LeakCanary.install(application);此时使用application进行registerActivityLifecycleCallbacks,从而来监听Activity的何时被destroy。
-
在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收,检测方式如以下步骤。
-
使用一个弱引用WeakReference指向这个activity,并且给这个弱引用指定一个引用队列queue,同时创建一个key来标识该activity。
-
然后将检测的方法ensureGone()投递到空闲消息队列。
-
当空闲消息执行的时候,去检测queue里面是否存在刚刚的弱引用,如果存在,则说明此activity已经被回收,就移除对应的key,没有内存泄漏发生。
-
如果queue里不存在刚刚的弱引用,则手动进行一次gc。
-
gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,否则没有发生内存泄漏,流程结束。
关键问题:
-
Activity销毁了,如何监听onDestroy?
通过Application注册Activity的生命周期回调,具体:通过application的registerActivityLifecycleCallbacks回调,在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收. -
为何要进行一次手动gc?为什么要放入空闲消息里面去执行?
Android的Gc过程是通过空闲消息实现的,优先级是很低。当MainLooper中没有消息执行时,就是空闲的,此时就会执行IdleHandlers里面的内容,gc才会得到执行。
注意: activity 的onDestroy()被调用了,只是说明该activity被销毁了,并不是说已经发生了gc.
我们的检测逻辑要放在gc之后,才能保证正确性,那就需要在mIdleHandlers执行之后了,但是,系统并没有提供比IdleHandlers优先级更低的工具,所以,我们也只能将我们的检测逻辑也放到IdleHandlers中;当检测逻辑运行时,大概率发生了gc,但也可能还没进行过gc,才能保证泄露结果的准确性。 -
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
一次分析检测多个泄漏并按照泄漏的类型分组
- 为什么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的改进:
-
监控机制
由LeakCanary的直接检查泄漏,改为:
采用内存阈值检测方式(当内存使用率达到80%以上),
周期性查询Java堆内存、线程数、文件描述符数等资源占用情况,
将对象是否泄漏的判断延迟到了解析时,
当连续多次超过设定阈值或突发性连续快速突破高阈值时,触发镜像采集,避免传统的频繁主动gc。
1)间隔5s检测一次
2)触发内存镜像采集的条件:
当内存使用率达到80%以上
两次检测时间内(例如5s内),内存使用率增加5% -
fork子进程,dump内存镜像–提高dump效率。
镜像采集采用虚拟机supend->fork虚拟机进程->虚拟机resume->dump内存镜像的策略,将传统Dump冻结进程20s的时间缩减至20ms以内。
我们知道LeakCanary检测内存泄漏,不能用于线上,是因为它dump内存镜像是在当前进程进行操作,会冻结App一段时间。
所以,作为线上OOM监控,KOOM会fork子进程dump内存镜像。
1)fork成功以后,父进程立刻恢复虚拟机运行,解除冻结;
2)主进程可以等待子进程dump结束,然后再返回执行内存镜像文件分析操作;
3)并且子进程dump内存镜像期间不会受到父进程数据变动的影响。 -
解析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组件,绕过了这一限制