内存问题大致可分为内存溢出和内存泄露二大类。
- 内存溢出(Out Of Memory) :就是申请内存时,剩余的空间(经过GC)没有足够的内存进行分配。
- 内存泄露 (Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。
对于内存溢出问题,一般是由于一次申请一块大内存、无数次申请小内存、创建过多线程、打开文件未关闭等等所引起,对于不同的原因引起的可以采用不同方式去监控告警处理。比如对于创建线程、打开文件未关闭的可以 hook 其创建线程、打开关闭文件时记录,并在进程结束时判断那些是打开未关闭的,但是对于内存分配来说,监控内存分配好像没有什么意义。
而内存泄露问题是由于生命周期长的对象持有了生命周期短的对象,导致生命周期短的对象没有办法及时释放内存,在 Android 中最经典的就是 activity 被 handle 的 msg 持有,而 msg 又在 app 进程的 msgqueue 中,没有执行 msg 就不会释放所持有的 activity 。
在本地可以使用 AS 自带的 profiler 很方便的来查看内存相关的东西,也可以记录下内存放到分析工具如 MAT 来分析。
而在线上实现起来有点困难,难在在什么时机去拿内存快照,怎么去拿内存快照及拿到之后需不需要本地分析、裁剪上传等问题。在网络上及大厂分享中有对这些问题比较完美的的解决方案。
拿内存快照的时机
- 类似与 LeakCanary 方案,主要监控 activity 和 fragment,原理是在 Activity.onDestroy() 后连续触发GC,并检查引用队列,判定 Activity 是否发生了泄漏,没有被回收则执行拿内存快照的操作。
- 美团分享及快手 KOOM 的方案,轮训去检测内存的使用,当依次从低到高达到阈值或突然到95%的使用时触发去内存快照操作。
怎么去拿内存快照
在 Android 中,可以用过 Debug.dumpHprofData("path") 来得到内存快照,但是这个过程一般会持续几秒到几十秒不等,为保持现场信息,在这个过程中会挂起所有的线程来进行,所以 dump 的过程中不可操作 app。
- 在子线程去拿内存快照,缺点在上方。
- 利用 Linux 的 Copy-On-Write 机制。挂起当前进程,开一个子进程之后恢复主进程,在子进程中拿内存快照,可以不影响到主线程的使用,卡顿的时间(fork 子进程的时间)几乎可以不计。
快照裁剪
对于内存快照的处理,一般有本地分析和传回后台分析两种。而快照的大小轻轻松松就可以达到 100MB 以上,无论是本地分析还是上传都需要对快照进行裁剪,删除其中无用的信息。
- 直接对 dump 的快照进行裁剪。但是很不幸的是当快照过大时再次触发OOM,直接GG。
- 在 dump 的过程中对快照进行裁剪。可以 hook 系统的 write 函数,当判断写入的是快照内容时直接进行裁剪。
快照分析
根据业务需求即可,使用 LeakCanary 使用的 haha 库或 shark 库,或者自己根据 hprof 文件格式自己分析都可。
参考:
Koom 解决hprof文件过大-源码解析
微信 Android 终端内存优化实践
快手客户端稳定性体系建设
java hprof 文件格式
...