抖音 Android 性能优化系列:Java 内存优化篇

内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of memory)崩溃 。抖音作为一款用户使用广泛的产品,需要在各种机器资源上保持优秀的流畅性和稳定性,内存优化是必须要重视的环节 。
 
本文从抖音 JAVA OOM 内存优化的治理实践出发,尝试给大家分享一下抖音团队关于 Java 内存优化中的一些思考,包括工具建设、优化方法论 。
抖音 Java OOM 背景在未对抖音内存进行专项治理之前我们梳理了一下整体内存指标的绝对值和相对崩溃,发现占比都很高 。另外,内存相关指标在去年春节活动时又再次激增达到历史新高,所以整体来看内存问题相当严峻,必须要对其进行专项治理 。抖音这边通过前期归因、工具建设以及投入一个双月的内存专项治理将整体 Java OOM 优化了百分之 80 。
Java OOM Top 堆栈归因在对抖音的 Java 内存优化治理之前我们先根据平台上报的堆栈异常对当前的 OOM 进行归因,主要分为下面几类:
抖音 Android 性能优化系列:Java 内存优化篇

文章插图
 
图 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 核心库在线下设计了一套自动分析上报内存泄露的工具,主要流程如下:
抖音 Android 性能优化系列:Java 内存优化篇

文章插图
 
图 2.线下自动分析流程
抖音在运行了一段线下的内存泄漏工具之后,发现了线下工具的各种弊端:
  1. 检测出来的内存泄漏过多,并且也没有比较好的优先级排序,研发消费不过来,历史问题就一直堆积 。另外也很难和业务研发沟通问题解决的收益,大家针对解决线下的内存泄漏问题的 ROI(投入产出比)比较难对齐 。
  2. 线下场景能跑到的场景有限,很难把所有用户场景穷尽 。抖音用户基数很大,我们经常遇到一些线上的 OOM 激增问题,因为缺少线上数据而无从查起 。
  3. Android 端的 HPORF 的获取依赖原生的 Debug.dumphporf,dump 过程会挂起主线程导致明显卡顿,线下使用体验较差,经常会有研发反馈影响测试 。
  4. LeakCanary 基于 Shark 分析引擎分析,分析速度较慢,通常在 5 分钟以上才能分析完成,分析过程会影响进程内存占用 。
  5. 分析结果较为单一,仅仅只能分析出 Fragment、Activity 内存泄露,像大对象、过多小对象问题导致的内存 OOM 无法分析 。


    推荐阅读