在对抖音的 Java 内存优化治理之前我们先根据平台上报的堆栈异常对当前的 OOM 进行归因,主要分为下面几类:
图 1. OOM 分类
其中 pthread_create
问题占到了总比例大约在百分之 50,Java 堆内存超限为百分之 40 多,剩下是少量的 fd 数量超限。其中 pthread_create
和 fd 数量不足均为 native 内存限制导致的 Java 层崩溃,我们对这部分的内存问题也做了针对性优化,主要包括:
-
线程收敛、监控
-
线程栈泄漏自动修复
-
FD 泄漏监控
-
虚拟内存监控、优化
-
抖音 64 位专项
治理之后 pthread_create
问题降低到了 0.02‰以下,这方面的治理实践会在下一篇抖音 Native 内存治理实践中详细介绍,大家敬请期待。本文重点介绍 Java 堆内存治理。
=======================================================================
从 Java 堆内存超限的分类来看,主要有两类问题:
1. 堆内存单次分配过大多次分配累计过大。
触发这类问题的原因有数据异常导致单次内存分配过大超限,也有一些是 StringBuilder 拼接累计大小过大导致等等。这类问题的解决思路比较简单,问题就在当前的堆栈。
2. 堆内存累积分配触顶。
这类问题的问题堆栈会比较分散,在任何内存分配的场景上都有可能会被触发,那些高频的内存分配节点发生的概率会更高,比如 Bitmap 分配内存。这类 OOM 的根本原因是内存累积占用过多,而当前的堆栈只是压死骆驼的最后一根稻草,并不是问题的根本所在。所以这类问题我们需要分析整体的内存分配情况,从中找到不合理的内存使用(比如内存泄露、大对象、过多小对象、大图等)。
====================================================================
工欲善其事,必先利其器。从上面的内存治理思路看,工具需要主要解决的问题是分析整体的内存分配情况,发现不合理的内存使用(比如内存泄露、大对象、过多小对象等)。
我们从线下和线上两个维度来建设工具:
线下
线下工具是最先考虑的,在研发和测试的时候能够提前发现内存泄漏问题。业界的主流工具也是这个思路,比如 Android Studio Memory Profiler、LeakCanary、Memory Analyzer (MAT)。
我们基于 LeakCanary 核心库在线下设计了一套自动分析上报内存泄露的工具,主要流程如下:
图 2.线下自动分析流程
抖音在运行了一段线下的内存泄漏工具之后,发现了线下工具的各种弊端:
-
检测出来的内存泄漏过多,并且也没有比较好的优先级排序,研发消费不过来,历史问题就一直堆积。另外也很难和业务研发沟通问题解决的收益,大家针对解决线下的内存泄漏问题的 ROI(投入产出比)比较难对齐。
-
线下场景能跑到的场景有限,很难把所有用户场景穷尽。抖音用户基数很大,我们经常遇到一些线上的 OOM 激增问题,因为缺少线上数据而无从查起。
-
Android 端的 HPORF 的获取依赖原生的
Debug.dumpHporf
,dump 过程会挂起主线程导致明显卡顿,线下使用体验较差,经常会有研发反馈影响测试。 -
LeakCanary 基于 Shark 分析引擎分析,分析速度较慢,通常在 5 分钟以上才能分析完成,分析过程会影响进程内存占用。
-
分析结果较为单一,仅仅只能分析出 Fragment、Activity 内存泄露,像大对象、过多小对象问题导致的内存 OOM 无法分析。
线上
正是由于上述一些弊端,抖音最早的线下工具和治理流程并没有起到什么太大作用,我们不得不重新审视一下,工具建设的重心从线下转成了线上。线上工具的核心思路是:在发生 OOM 或者内存触顶等触发条件下,dump 内存的 HPROF 文件,对 HPROF 文件进行分析,分析出内存泄漏、大对象、小对象、图片问题并按照泄露链路自动归因,将大数据问题按照用户发生次数、泄露大小、总大小等纬度排序,推进业务研发按照优先级顺序来建立消费流程。为此我们研发了一套基于 HPORF 分析的线下、线上闭环的自动化分析工具 Liko(寓意 ko 内存 Leak 问题)。
Liko 整体架构
图 3. Liko 架构图
整体架构由客户端、Server 端和核心分析引擎三部分构成。
- 客户端
在客户端完成 HPROF 数据采集和分析(针对端上分析模式),这里线上和线下策略不同。
线上:主要在 OOM 和内存触顶时通过用户无感知 dump 来获取 HPROF 文件,当 App 退出到后台且内存充足的情况进行分析,为了尽量减少对 App 运行时影响,主要通过裁剪 HPROF 回传进行分析,减轻服务器压力,对部分比例用户采用端上分析作为 Backup。
线下:dump 策略配置较为激进,在 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值多种场景下触发 dump,并实时在端上分析上传至后台并在本地自动生成 html 报表,帮助研发提前发现可能存在的内存问题。
- Server 端
Server 端根据线上回传的大数据完成链路聚合、还原、分配,并根据用户发生次数、泄露大小、总大小等纬度促进研发测消费,对于回传分析模式则会另外进行 HPORF 分析。
- 分析引擎
基于 MAT 分析引擎完成内存泄露、大对象、小对象、图片等自动归因,同时支持在线下自动生成 Html 报表。
Liko 流程图
图 4. Liko 流程图
整体流程分为
-
Hprof 收集
-
分析时机
-
分析策略
Hprof 收集
收集过程我们设置了多种策略可以自由组合,主要有 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值时触发,线下线上策略配置不同。
为了解决 dump 挂起进程问题,我们采用了子进程 dump+fileObsever 的方式完成 dump 采集和监听。
在 fork 子进程之前先 Suspend
获取主进程中的线程拷贝,通过 fork 系统调用创建子进程让子进程拥有父进程的拷贝,然后 fork 出的子进程中调用 Hprof 的 DumpHeap
函数即可完成把耗时的 dump 操作在放在子进程。由于 suspend
和 resume
是系统函数,我们这里通过自研的 native hook 工具对 libart.so
hook 获取系统调用。由于写入是在子进程完成的,我们通过 Android 提供的 fileObsever 文件写入进行监控获取 dump 完成时机。
图 5.子进程 dump 流程图
Hprof 分析时机
为了达到分析过程对于用户无感,我们在线上、线下配置了不同的分析时机策略,线下在 dump 分析完成后根据内存状态主动触发分析,线上当用户下次冷启退出应用后台且内存充足的情况下触发分析。
分析策略
分析策略我们提供了两种,一种在 Android 客户端分析,一种回传至 Server 端分析,均通过 MAT 分析引擎进行分析。
端上分析
分析引擎
端上分析引擎的性能很重要,这里我们主要对比了 LeakCanary 的分析引擎 Shark 和 Haha 库的 MAT。
图 6. Shark VS MAT
我们在相同客户端环境对 160M 的 HPROF 多次分析对比发现 MAT 分析速度明显优于 Shark,另外针对 MAT 分析后仍持有统治者树占用内存我们也做了主动释放,对比性能收益后采用基于 MAT 库的分析引擎进行分析,对内存泄漏引用链路自动归并、大对象小对象引用链自动分析、大图线下自动还原线上过滤无用链路,分析结果如下:
内存泄漏
图 7. 内存泄漏链路
对泄漏的 Activity 的引用链进行了聚合分析,方便一次性解决该 Activity 的泄漏链释放内存。
大对象
图 8. 大对象链路
大对象不止分析了引用链路,还递归分析了内部 top 持有对象(InRefrenrece
)的 RetainedSize。
小对象
图 9. 小对象链路
小对象我们对 top 的外部持有对象(OutRefrenrece
)进行聚合得到占有小对象最多的链路。
图片
图 10. 图片链路
图片我们过滤了图片库等无效引用且对 Android 8.0 以下的大图在线下进行了还原。
回传分析
为了最大限度的节省用户流量且规避隐私风险,我们通过自研 HPROF 裁剪工具 Tailor 在 dump 过程对 HPROF 进行了裁剪。
裁剪过程
图 11. Tailor 裁剪流程
去除了无用信息
-
跳过 header
-
分 tag 裁剪
-
裁剪无用信息:char[]; byte[]; timestamp; stack trace serial number; class serial number;
-
压缩数据信息
同时对数据进行 zlib 压缩,在 server 端数据还原,整体裁剪效果:180M—>50M---->13M
====================================================================
除了通过后台根据 GCROOT+引用链自动分配研发跟进解决我们常见的内存泄漏外,我们还对系统导致一些内存泄漏进行了分析和修复。
系统异步 UI 泄漏
根据上传聚合的引用链我们发现在 Android 6.0 以下有一个 HandlerThread 作为 GCROOT 持有大量 Activity 导致内存泄漏,根据引用发现这些泄漏的 Activity 都被一个 Runnable(这里是 Runnable 是一个系统事件 SendViewStateChangedAccessibilityEvent
)持有,这些 Runnable 被添加到一个 RunQueuel 中,这个队列本身被 TheadLocal 持有。
图 12. HandlerThread 泄露链路
我们从 SendViewStateChangedAccessibilityEvent
入手对源码进行了分析发现它在 notifyViewAccessibilityStateChangedIfNeeded
中被抛出,系统的大量 view 都会在自身的一些 UI 方法(eg: setChecked
)中触发改函数。
SendViewStateChangedAccessibilityEvent
的 runOrPost
方法会走到我们常用的 View 的 postDelay
方法中,这个方法在当 view 还未被 attched 到根 view 的时候会加入到一个 runQueue 中。
这个 runQueue 会在主线程下一次的 performTraversals()
中消费掉。
如果这个 runQueue 不在主线程那就没有消费的机会。
根据上面的分析发现造成这种内存泄漏需要满足一些条件:
-
view 调用了
postDelay
方法 (这里是notifyViewAccessisbilityStateChangeIfNeeded
触发) -
view 处于 detached 状态
-
上述过程是在非主线程里面操作的,ThreadLocal 非 UIThread,持有的 runQueue 不会走
performTraversals
消费掉。
抖音这边大量使用了异步 UI 框架来优化渲染性能,框架内部由一个 HandlerThread 驱动,完全符合上述条件。针对该问题,我们通过反射获取非主线程的 ThreadLocal,在每次异步渲染完主动清理内部的 RunQueue。
图 13. 反射清理流程
另外,Google 在 6.0 上也修复了 notifyViewAccessisbilityStateChangeIfNeeded
的判断不严谨问题。
内存泄漏兜底
大量的内存泄漏,如果我们都靠推进研发解决,经常会出现生产大于消费的情况,针对这些未被消费的内存泄漏我们在客户端做了监控和止损,将 onDestory 的 Activity 添加到 WeakRerefrence
中,延迟 60s 监控是否回收,未回收则主动释放泄漏的 Activity 持有的 ViewTree 的背景图和 ImageView 图片。
主要对三种类型的大对象进行优化
-
全局缓存:针对全局缓存我们按需释放和降级了不需要的缓存,尽量使用弱引用代替强引用关系,比如针对频繁泄漏的 EventBus 我们将内部的订阅者关系改为弱引用解决了大量的 EventBus 泄漏。
-
系统大对象:系统大对象如
PreloadDrawable
、JarFile
我们通过源码分析确定主动释放并不干扰原有逻辑,在启动完成或在内存触顶时主动反射释放。 -
动画:用原生动画代替了内存占用较大的帧动画,并对 Lottie 动画泄漏做了手动释放。
图 14. 大对象优化点
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
最后
如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
dnimg.cn/13f2cb2e05a14868a3f0fd6ac81d625c.png)
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-MHGAIRBY-1711998319643)]
最后
如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
[外链图片转存中…(img-PRTGqhbx-1711998319643)]
最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!