1 KOOM使用
KOOM简介与接入
KOOM(Kwai OOM, Kill OOM)是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。
高性能线上内存监控方案,github.com/KwaiAppTeam…
依赖配置:
项目根目录 build.gradle 中增加 mavenCentral
repositories {
mavenCentral()
}
项目 app/build.gradle 中增加依赖
dependencies {
implementation "com.kuaishou.koom:koom-java-leak:2.2.0"
}
KOOM初始化
1、初始化 MonitorManager
2、初始化并开启 OOMMonitor
注意事项
1、一定需要在自定义的Application中初始化MonitorManager;
KOOM会开启IntentService启动hprof(内存快照)解析,该Service注册于子进程: heap_analysis!
2、MonitorManager初始化为非DebugMode,则一个APP版本:
- 第一次触发OOM完成dump与分析内存后,超过15天再次触发OOM,不再dump与分析内存快照;
- 触发四次OOM完成dump与分析内存后,第五次不再dump与分析内存快照;
- 15天与5次可在初始化MonitorManager时配置
3、KOOM不是内存泄漏时触发内存的dump与分析,而是周期性检测,在满足以下情况触发:
- APP内存占用率超过内存阈值(默认:),且连续三次检测中,后一次检测没有比前一次降低0.05;
- 线程数量超过阈值,且连续三次检测中,后一次检测没有比前一次检测减少50个线程;
- 文件描述符超过阈值,且连续三次检测中,后一次检测没有比前一次检测减少50个文件打开数;
- APP内存占用率超过阈值;
- 当前内存占用率-上次检测内存占用率大于阈值(内存占用率增长过快);
4、为什么LeakCanary不能用于线上?
- LeakCanary通过弱引用关联对象连续两次主动触发GC判定是否泄露,频繁GC易导致程序卡顿;
- 每次检测到泄露都会dump快照文件,同一个泄露多次触发也会dump多份快照;
- dump hprof文件(内存镜像)耗时,易造成程序长时间无响应;
- hprof文件太大,解析耗时;
2 KOOM原理
1. KOOM 的整体架构
KOOM 的主要模块包括以下几部分:
-
内存监控模块:
- 监控内存的使用情况,捕获 OOM 前的状态。
- 提供稳定性保障,通过轻量化的堆采样减少性能开销。
-
堆转储模块:
- 在内存达到特定阈值或发生 OOM 崩溃时,捕获堆转储文件。
-
堆分析模块:
- 使用定制化的堆分析引擎,快速分析转储文件,定位内存问题。
-
结果反馈与优化:
- 提供内存泄漏、对象冗余和大对象使用等问题的分析报告。
- 提供基于运行时的解决方案,例如释放缓存。
2. 核心工作原理
(1) 内存监控
KOOM 通过监听应用内存的使用情况,结合多种触发条件进行堆分析或问题定位。其触发条件包括:
-
Native 层监控: 使用
/proc/self/status
和mmap
机制监控应用的内存占用,如 VSS、RSS、PSS 和 Native 堆。 -
Java 层监控: 通过
Runtime.getRuntime()
获取 Java 堆使用信息,例如已分配堆大小和最大堆大小。 -
触发条件:
- 内存使用超过阈值(如 PSS > 80%)。
- OOM 崩溃时。
- 定时触发。
(2) 堆转储
KOOM 在触发条件满足时,会通过轻量化的方式进行堆转储:
- Android 5.0+:使用 ART 提供的
Debug.dumpHprofData
方法。 - Android 4.x:通过底层
malloc_debug
实现类似功能。
为了避免影响性能,KOOM 使用增量转储或部分转储,避免在 OOM 高风险时因堆转储导致应用直接崩溃。
(3) 堆分析
KOOM 的堆分析基于自研引擎进行优化,针对移动设备的内存限制进行了轻量化设计:
-
堆数据解析:
- 分析
.hprof
文件中的对象分布、引用关系和大小。 - 构建内存引用图,查找内存泄漏路径。
- 分析
-
优化分析流程:
- KOOM 引入采样分析,跳过部分小对象和不重要的引用路径,只分析大对象和关键对象(如
Activity
、Bitmap
)。
- KOOM 引入采样分析,跳过部分小对象和不重要的引用路径,只分析大对象和关键对象(如
-
问题定位:
- 内存泄漏:查找长生命周期对象与短生命周期对象的引用关系。
- 大对象检测:分析是否存在 Bitmap、缓存等对象占用过多内存。
- 对象冗余:检测是否存在重复分配的对象。
(4) OOM 崩溃保护
KOOM 提供了以下功能来降低 OOM 崩溃风险:
-
释放内存:
- 在内存占用过高时,自动释放缓存或可重构的资源。
- 针对性清理部分生命周期较短的对象。
-
预警机制:
- 在即将发生 OOM 时,通过日志和上报机制通知开发者。
3. 核心模块源码解析
以下是 KOOM 的几个核心模块及其原理解析:
(1) 监控模块
功能:通过实时监控应用的内存状态,触发堆转储或其他操作。
关键类:MemoryMonitor
- 原理:
使用定时任务或内存状态监听器,定期获取内存信息。 结合 Java 堆和 Native 堆的监控,判断是否触发内存转储。
kotlin
复制代码
class MemoryMonitor(private val onThresholdExceeded: () -> Unit) {
fun startMonitoring() {
// 定时检查内存占用
Timer().scheduleAtFixedRate(object : TimerTask() {
override fun run() {
val currentPss = MemoryUtils.getPss()
if (currentPss > THRESHOLD) {
onThresholdExceeded()
}
}
}, 0, MONITOR_INTERVAL)
}
}
getPss
方法:
通过 /proc/self/statm
或 Android 提供的 API 获取进程内存状态。
- 周期轮询检测:
不再关注某个具体对象,通过轮询检测是否出现内存泄漏!
1、内存占用率检测
2、线程数检测
3、文件描述符检测
4、内存增长检测
- 内存检测
HeapOOMTracker:
1、连续三次检测处于高内存占用状态且没有出现明显下降触发;
FastHugeMemoryOOMTracker:
2、超过内存占用率阈值触发;
3、短时间内内存占用量快速增长,超过350M触发!
- 线程与文件描述符检测
连续三次检测处于高线程数状态且没有出现明显下降触发;
N+1次检测虽然超过阈值,但是比第N次检测减少了50以上的线程数,则认为线程数在有效降低,此时重置,否则触发。
1、线程数超过阈值(默认:750,华为:450)
2、本次检测线程数不少于上次检测线程数-50
(2) 转储模块
功能:当内存状态达到预警值时,捕获堆快照供后续分析。
关键类:HeapDumper
-
堆转储实现: 使用
Debug.dumpHprofData
方法生成堆转储文件。 -
源码解析:
kotlin 复制代码 object HeapDumper { fun dumpHeap(outputPath: String): Boolean { return try { Debug.dumpHprofData(outputPath) true } catch (e: Exception) { false } } }
-
特点:
- 异步转储,避免阻塞主线程。
- 支持增量或部分转储,降低性能开销。
Dump hprof
hprof是基于JVMTI 实现的内存分析器代理,其转储文件记录了 Java 的 内存镜像(Heap Profile),其中记录了内存堆详细的使用信息,可用于分析Java程序内存的各种性能问题。
虚拟机提供的 Debug.dumpHprofData 可以将hprof文件输出在指定的文件中,但是这个过程会 “冻结” 整个应用进程,造成数秒甚至数十秒内用户无法操作!(能否在子线程中dump hprof?)
fork子进程
fork系统调用用于创建一个新进程,称为子进程,它与调用fork的进程(父进程)同时运行。fork一次调用会返回多次,其中:
n 负值:创建子进程失败。
n 零: 返回到新创建的子进程。
n 正值:返回新创建的子进程的进程ID
子进程的内存镜像就是父进程的!在子进程中完成内存dump
fork与多线程
在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中,其他线程均在子进程中立即停止并消失。但父进程全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留!
因此,当子进程中进行dump hprof时,SuspendAll触发暂停是永远等不到其他线程返回结果,从而导致子进程阻塞卡死!
KOOM内存dump
先在主进程执行SuspendAll,使ThreadList中保存的所有线程状态为suspend,之后fork,此时子进程执行dumphprof,由于其共享父进程的ThreadList全局变量,因此认为全部线程已经处于suspend状态,避免了子进程dump时SuspendAll触发暂停等不到线程返回结果的情况。fork完毕父进程即可立刻执行ResumeAll恢复运行。
在Android中,suspend相关代码被编译成libart.so(Art),调用libart.so库中的代码需要用到动态加载
动态加载API
(3) 分析模块
功能:解析堆转储文件,生成内存分析报告。
关键类:HeapAnalyzer
-
原理: 使用 KOOM 自研的轻量化 Shark 引擎,对堆转储文件进行解析和引用图构建。
-
源码解析:
kotlin 复制代码 class HeapAnalyzer { fun analyzeHeap(heapDumpFile: File): HeapAnalysisResult { // 加载堆文件并解析 val parser = HeapDumpParser(heapDumpFile) val referenceGraph = parser.parse() return analyzeLeaks(referenceGraph) } private fun analyzeLeaks(graph: ReferenceGraph): HeapAnalysisResult { // 从 GC Root 开始追踪未释放对象 return graph.findLeaks() } }
-
Leak 检测: 通过引用链从 GC Root 出发,查找长生命周期对象与短生命周期对象的引用关系,定位内存泄漏。
(4) 崩溃保护模块
功能:当内存接近 OOM 时,通过释放缓存等手段降低崩溃风险。
关键类:OOMProtection
-
逻辑:
- 监控内存占用。
- 触发缓存清理或其他降级操作。
-
源码解析:
kotlin 复制代码 object OOMProtection { fun protect() { if (MemoryUtils.isNearOOM()) { CacheManager.clear() System.gc() } } }
-
特点:
- 分级保护:根据内存占用程度采取不同措施。
- 自动化清理:对可释放资源进行清理。
(5) 工具类
KOOM 的工具类负责提供辅助功能,例如获取内存信息、日志记录等。
关键类:MemoryUtils
-
获取进程内存占用:
kotlin 复制代码 object MemoryUtils { fun getPss(): Long { // 从 /proc/self/statm 文件读取 PSS val statusFile = File("/proc/self/status") return parsePss(statusFile) } }
-
判断是否接近 OOM:
kotlin 复制代码 fun isNearOOM(): Boolean { val pss = getPss() return pss > NEAR_OOM_THRESHOLD }
4. KOOM 的优势
(1) 高效轻量化
- 通过采样和增量分析,减少性能开销。
- 适配移动设备的内存限制。
(2) 全流程覆盖
- 从内存监控到堆转储,再到堆分析,全流程优化。
(3) 实时保护
- 提供内存预警和 OOM 崩溃保护功能。
(4) 易用性
- 开发者只需简单集成,KOOM 即可自动监控内存状态。
5. 应用场景
- 内存泄漏检测:快速定位和修复导致 OOM 的内存泄漏。
- 内存优化:检测大对象或不必要的缓存占用。
- 崩溃保护:降低 OOM 崩溃率,提高应用稳定性。
6. 结论
KOOM 是一款专注于移动端 OOM 问题的内存优化工具,其工作原理包括:
- 内存监控:实时监控 Java 和 Native 层内存使用情况。
- 堆转储:在合适时机捕获内存堆信息。
- 堆分析:通过引用链和大对象分析,定位内存问题。
- 崩溃保护:提供内存清理和预警机制,降低 OOM 风险。
KOOM 的轻量化设计、高效堆分析能力和易用性,使其成为解决 Android 内存问题的强大工具。