内存作为计算机程序运行最重要嘚资源之一需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏重则导致用户应鼡程序发生 OOM(out of memory)崩溃。抖音作为一款用户使用广泛的产品需要在各种机器资源上保持优秀的流畅性和稳定性,内存优化渲染为性能和内存是必须要重视的环节
本文从抖音 Java OOM 内存优化渲染为性能和内存的治理实践出发,尝试给大家分享一下抖音团队关于 Java 内存优化渲染为性能囷内存中的一些思考包括工具建设、优化渲染为性能和内存方法论。
在未对抖音内存进行专项治理之前我们梳理了一下整体内存指标的絕对值和相对崩溃发现占比都很高。另外内存相关指标在去年春节活动时又再次激增达到历史新高,所以整体来看内存问题相当严峻必须要对其进行专项治理。抖音这边通过前期归因、工具建设以及投入一个双月的内存专项治理将整体 Java OOM 优化渲染为性能和内存了百分之 80
在对抖音的 Java 内存优化渲染为性能和内存治理之前我们先根据平台上报的堆栈异常对当前的 OOM 进行归因,主要分为下面几类:
层崩溃我们對这部分的内存问题也做了针对性优化渲染为性能和内存,主要包括:
治理之后 pthread_create
问题降低到了 0.02‰以下这方面的治理实践会在下一篇抖音 Native 內存治理实践中详细介绍,大家敬请期待本文重点介绍 Java 堆内存治理。
从 Java 堆内存超限的分类来看主要有两类问题:
1. 堆内存单次分配过大/哆次分配累计过大。
触发这类问题的原因有数据异常导致单次内存分配过大超限也有一些是 StringBuilder 拼接累计大小过大导致等等。这类问题的解決思路比较简单问题就在当前的堆栈。
这类问题的问题堆栈会比较分散在任何内存分配的场景上都有可能会被触发,那些高频的内存汾配节点发生的概率会更高比如 Bitmap 分配内存。这类 OOM 的根本原因是内存累积占用过多而当前的堆栈只是压死骆驼的最后一根稻草,并不是問题的根本所在所以这类问题我们需要分析整体的内存分配情况,从中找到不合理的内存使用(比如内存泄露、大对象、过多小对象、夶图等)
工欲善其事,必先利其器从上面的内存治理思路看,工具需要主要解决的问题是分析整体的内存分配情况发现不合理的内存使用(比如内存泄露、大对象、过多小对象等)。
我们从线下和线上两个维度来建设工具:
我们基于 LeakCanary 核心库在线下设计了一套自动分析仩报内存泄露的工具主要流程如下:
图 2.线下自动分析流程
抖音在运行了一段线下的内存泄漏工具之后,发现了线下工具的各种弊端:
-
检測出来的内存泄漏过多并且也没有比较好的优先级排序,研发消费不过来历史问题就一直堆积。另外也很难和业务研发沟通问题解决嘚收益大家针对解决线下的内存泄漏问题的 ROI(投入产出比)比较难对齐。
-
线下场景能跑到的场景有限很难把所有用户场景穷尽。抖音鼡户基数很大我们经常遇到一些线上的 OOM 激增问题,因为缺少线上数据而无从查起
-
Android 端的 HPORF 的获取依赖原生的 Debug.dumpHporf
,dump 过程会挂起主线程导致明显鉲顿线下使用体验较差,经常会有研发反馈影响测试
-
LeakCanary 基于 Shark 分析引擎分析,分析速度较慢通常在 5 分钟以上才能分析完成,分析过程会影响进程内存占用
-
分析结果较为单一,仅仅只能分析出 Fragment、Activity 内存泄露像大对象、过多小对象问题导致的内存 OOM 无法分析。
正是由于上述一些弊端抖音最早的线下工具和治理流程并没有起到什么太大作用,我们不得不重新审视一下工具建设的重心从线下转成了线上。线上笁具的核心思路是:在发生 OOM 或者内存触顶等触发条件下dump 内存的 HPROF 文件,对 HPROF
文件进行分析分析出内存泄漏、大对象、小对象、图片问题并按照泄露链路自动归因,将大数据问题按照用户发生次数、泄露大小、总大小等纬度排序推进业务研发按照优先级顺序来建立消费流程。为此我们研发了一套基于 HPORF 分析的线下、线上闭环的自动化分析工具 Liko(寓意 ko 内存 Leak 问题)
整体架构由客户端、Server 端和核心分析引擎三部分构荿。
在客户端完成 HPROF 数据采集和分析(针对端上分析模式)这里线上和线下策略不同。
线上:主要在 OOM 和内存触顶时通过用户无感知 dump 来获取 HPROF 攵件当 App 退出到后台且内存充足的情况进行分析,为了尽量减少对 App 运行时影响主要通过裁剪 HPROF 回传进行分析,为减轻服务器压力对部分仳例用户采用端上分析作为 Backup。
线下:dump 策略配置较为激进在 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值多种场景下触发 dump,并实時在端上分析上传至后台并在本地自动生成 html 报表帮助研发提前发现可能存在的内存问题。
Server 端根据线上回传的大数据完成链路聚合、还原、分配并根据用户发生次数、泄露大小、总大小等纬度促进研发测消费,对于回传分析模式则会另外进行 HPORF 分析
基于 MAT 分析引擎完成内存泄露、大对象、小对象、图片等自动归因,同时支持在线下自动生成 Html 报表
收集过程我们设置了多种策略可以自由组合,主要有 OOM、内存触頂、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值时触发线下线上策略配置不同。
文件写入进行监控获取 dump 完成时机
为了达到分析过程对于鼡户无感,我们在线上、线下配置了不同的分析时机策略线下在 dump 分析完成后根据内存状态主动触发分析,线上当用户下次冷启退出应用後台且内存充足的情况下触发分析
分析策略我们提供了两种,一种在 Android 客户端分析一种回传至 Server 端分析,均通过 MAT 分析引擎进行分析
端上汾析引擎的性能很重要,这里我们主要对比了 LeakCanary 的分析引擎 Shark 和 Haha 库的 MAT
我们在相同客户端环境对 160M 的 HPROF 多次分析对比发现 MAT 分析速度明显优于 Shark,另外針对 MAT 分析后仍持有统治者树占用内存我们也做了主动释放对比性能收益后采用基于 MAT 库的分析引擎进行分析,对内存泄漏引用链路自动归並、大对象小对象引用链自动分析、大图线下自动还原线上过滤无用链路分析结果如下:
图 7. 内存泄漏链路
对泄漏的 Activity 的引用链进行了聚合汾析,方便一次性解决该 Activity 的泄漏链释放内存
小对象我们对 top 的外部持有对象(OutRefrenrece
)进行聚合得到占有小对象最多的链路。
图片我们过滤了图爿库等无效引用且对 Android 8.0 以下的大图在线下进行了还原
为了最大限度的节省用户流量且规避隐私风险,我们通过自研 HPROF 裁剪工具 Tailor 在 dump 过程对 HPROF 进行叻裁剪
除了通过后台根据 GCROOT+ 引用链自动分配研发跟进解决我们常见的内存泄漏外,我们还对系统导致一些内存泄漏进行了分析和修复
如果这个 runQueue 不在主线程那就没有消费的机会。
根据上面的分析发现造成这种内存泄漏需要满足一些条件:
抖音这边大量使用了异步 UI 框架来优化渲染为性能和内存渲染性能框架内部由一个 HandlerThread 驱动,完全符合上述条件针对该问题,我们通过反射获取非主线程的 ThreadLocal在每次异步渲染完主动清理内部的 RunQueue。
图 13. 反射清理流程
大量的内存泄漏如果我们都靠推进研发解决,经常会出现生产大于消费的情况针对这些未被消费的內存泄漏我们在客户端做了监控和止损,将 onDestory 的 Activity 添加到 WeakRerefrence 中延迟 60s 监控是否回收,未回收则主动释放泄漏的 Activity 持有的 ViewTree 的背景图和 ImageView 图片
主要对三種类型的大对象进行优化渲染为性能和内存
图 14. 大对象优化渲染为性能和内存点
小对象优化渲染为性能和内存我们集中在字段优化渲染为性能和内存、业务优化渲染为性能和内存、缓存优化渲染为性能和内存三个纬度不同的纬度有不同的优化渲染为性能和内存策略。
图 15. 小对象优化渲染为性能和内存思路
茬抖音的业务中视频是最核心且通用的 Model,抖音业务层的数据存储分散在各个业务维护了各自视频的 ModelModel 本身由于聚合了各个业务需要的属性很多导致单个实例内存占用就不低,随着用户使用过程实例增长内存占用越来越大对 Model 本身我们可以从属性优化渲染为性能和内存和拆汾这两种思路来优化渲染为性能和内存。
-
字段优化渲染为性能和内存:针对一次性的属性字段在使用完之后及时清理掉缓存,比如在视頻 Model 内部存在一个 Json 对象在反序列完成之后 Json 对象就没有使用价值了,可以及时清理
-
类拆分:针对通用 Model 冗杂过多的业务属性,尝试对 Model 本身进荇治理将各个业务线需要用到的属性进行梳理,将 Model 拆分成多个业务 Model 和一个通用 Model采用组合的方式让各个业务线最小化依赖自己的业务 Model,減少大杂烩 Model 不必要的内存浪费
上面提到的视频 Model,抖音最早使鼡 Manager 来管理通用的视频实例Manager 使用 HashMap 存储了所有的视频对象,最初的方案里面没有对内存大小进行限制且没有清除逻辑随着使用时间的增加洏不断膨胀,最终出现 OOM 异常为了解决视频 Model 无限膨胀的问题设计了一套缓存框架主要流程如下:
图 16. 视频缓存框架
使用 LRU 缓存机制来缓存视频對象。在内存中缓存最近使用的 100 个视频对象当视频对象从内存缓存中移除时,将其缓存至磁盘中在获取视频对象时,首先从内存中获取若内存中没有缓存该对象,则从磁盘缓存中获取在退出 App 时,清除 Manager 的磁盘缓存避免磁盘空间占用不断增长。
关于图片优化渲染为性能和内存我们主要从图片库的管理和图片本身优化渲染为性能和内存两个方面思考。同时对不合理的图片使用也做了兜底和监控
针对應用内图片的使用状况对图片库设置了合理的缓存,同时在应用 or 系统内存吃紧的情况下主动释放图片缓存
我们知道图片内存大小公式 = 图爿分辨率 * 每个像素点的大小。
图片分辨率我们通过设置合理的采样来减少不必要的像素浪费
//请求图片时,传入resize的大小一般直接取View的宽高
而单个像素大小,我们通过替换系统 drawable 默认色彩通道将部分没有透明通道的图片格式由 ARGB_8888 替换为 RGB565,在图片质量上的损失几乎肉眼不可见洏在内存上可以直接节省一半。
图 17. 图片兜底流程
关于对不合理的大图 or 图片使用我们在字节码层面进行了拦截和监控在原生 Bitmap or 图片库创建时機记录图片信息,对不合理的大图进行上报;另外在 ImageView 的设置过程中针对 Bitmap 远超过 view 本身超过大小的场景也进行了记录和上报
图 18. 图片字节码监控方案
是不是解决了 OOM 内存问题就告一段落了呢?作为一只追求极致的团队我们除了解决静态的内存占用外也自研了 Kenzo(Memory Insight)工具尝试解决动態内存分配造成的 GC 卡顿。
Kenzo 整体分为两个部分:
生产端主要以 Java 进行 API 调用,C++完成底层检测逻辑通过 JNI 完成底层逻辑控制。
消费端主要以 Python 完成数据的解析、视图合成以 HTML 完成页面內容展示。
基于动态内存监控我们对最为核心的启动场景的内存分配进行了归因分析优化渲染为性能和内存了一些头部的内存节点分配:
圖 23.启动阶段内存节点归因
另外我们也发现启动阶段存在大量的字符串拼接操作,虽然编译器已经优化渲染为性能和内存成了 StringBuider append
但是深入 StringBuider 源碼分析仍在存在大量的动态扩容动作(System.copy),为了优化渲染为性能和内存高频场景触发动态扩容的性能损耗在 StringBuilder
如果你觉得这篇内容对你还蠻有帮助,我想邀请你帮我三个小忙:
点赞转发,有你们的 『点赞和评论』才是我创造的动力。
关注公众号 『 Java斗帝 』不定期分享原創知识。
同时可以期待后续文章ing?