Android-性能优化-02-内存优化-KOOM

898 阅读10分钟

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

image.png

2、初始化并开启 OOMMonitor

image.png

注意事项

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 的主要模块包括以下几部分:

  1. 内存监控模块

    • 监控内存的使用情况,捕获 OOM 前的状态。
    • 提供稳定性保障,通过轻量化的堆采样减少性能开销。
  2. 堆转储模块

    • 在内存达到特定阈值或发生 OOM 崩溃时,捕获堆转储文件。
  3. 堆分析模块

    • 使用定制化的堆分析引擎,快速分析转储文件,定位内存问题。
  4. 结果反馈与优化

    • 提供内存泄漏、对象冗余和大对象使用等问题的分析报告。
    • 提供基于运行时的解决方案,例如释放缓存。

2. 核心工作原理

(1) 内存监控

KOOM 通过监听应用内存的使用情况,结合多种触发条件进行堆分析或问题定位。其触发条件包括:

  • Native 层监控: 使用 /proc/self/statusmmap 机制监控应用的内存占用,如 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 的堆分析基于自研引擎进行优化,针对移动设备的内存限制进行了轻量化设计:

  1. 堆数据解析

    • 分析 .hprof 文件中的对象分布、引用关系和大小。
    • 构建内存引用图,查找内存泄漏路径。
  2. 优化分析流程

    • KOOM 引入采样分析,跳过部分小对象和不重要的引用路径,只分析大对象和关键对象(如 ActivityBitmap)。
  3. 问题定位

    • 内存泄漏:查找长生命周期对象与短生命周期对象的引用关系。
    • 大对象检测:分析是否存在 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、内存增长检测
  • 内存检测

image.png HeapOOMTracker:

1、连续三次检测处于高内存占用状态且没有出现明显下降触发;

FastHugeMemoryOOMTracker:

2、超过内存占用率阈值触发;

3、短时间内内存占用量快速增长,超过350M触发!

  • 线程与文件描述符检测

image.png 连续三次检测处于高线程数状态且没有出现明显下降触发;

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?)

image.png

fork子进程

fork系统调用用于创建一个新进程,称为子进程,它与调用fork的进程(父进程)同时运行。fork一次调用会返回多次,其中:

n 负值:创建子进程失败。

n 零: 返回到新创建的子进程。

n 正值:返回新创建的子进程的进程ID

子进程的内存镜像就是父进程的!在子进程中完成内存dump

image.png

fork与多线程

在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中,其他线程均在子进程中立即停止并消失。但父进程全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留!

因此,当子进程中进行dump hprof时,SuspendAll触发暂停是永远等不到其他线程返回结果,从而导致子进程阻塞卡死!

image.png


KOOM内存dump

先在主进程执行SuspendAll,使ThreadList中保存的所有线程状态为suspend,之后fork,此时子进程执行dumphprof,由于其共享父进程的ThreadList全局变量,因此认为全部线程已经处于suspend状态,避免了子进程dump时SuspendAll触发暂停等不到线程返回结果的情况。fork完毕父进程即可立刻执行ResumeAll恢复运行。

image.png

在Android中,suspend相关代码被编译成libart.so(Art),调用libart.so库中的代码需要用到动态加载

image.png

动态加载API

image.png

(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 问题的内存优化工具,其工作原理包括:

  1. 内存监控:实时监控 Java 和 Native 层内存使用情况。
  2. 堆转储:在合适时机捕获内存堆信息。
  3. 堆分析:通过引用链和大对象分析,定位内存问题。
  4. 崩溃保护:提供内存清理和预警机制,降低 OOM 风险。

KOOM 的轻量化设计、高效堆分析能力和易用性,使其成为解决 Android 内存问题的强大工具。