KOOM 浅析

简介: KOOM 浅析

KOOM 相比较 LeakCanary 和 Matrix 来说有点不同,后俩者由于 dump 的整个过程会影响到主进程,所以基本应用与线下监控,而 KOOM 提出了 fork dump 的概念,能在 dump 分析内存泄漏的时候而不影响到主进程的应用运行,所以,非常适合使用在线上监控。


所有的内存泄漏监控工具都离不开这三点:


  • 监控触发时机
  • dump 内存堆栈
  • 分析 hprof 文件

1、监控触发时机


LeakCanary 和 Matrix 都是在 Activity.onDestroy 时触发泄漏检测,KOOM 有点另辟蹊径,KOOM 是用阈值检测法来触发,我们来看下核心逻辑:


MonitorThread.class

class MonitorRunnable implements Runnable {
    ...
    @Override
    public void run() {
      if (stop) {
        return;
      }
      // 是否触发检测
      if (monitor.isTrigger()) {
        // 检测回调触发
        stop = monitorTriggerListener
            .onTrigger(monitor.monitorType(), monitor.getTriggerReason());
      }
      if (!stop) {
        // 间隔 5s 轮训检测
        handler.postDelayed(this, monitor.pollInterval());
      }
    }
  }
复制代码


MonitorThread 是一个利用 HandlerThread 不停在轮训监控当前是否触发检测,isTrigger 是关键所在


HeapMonitor.class

@Override
  public boolean isTrigger() {
    ...
  // ①、获取当前的内存状态
    HeapStatus heapStatus = currentHeapStatus();
  // ②、当前使用内存是否达到最大阈值,内存使用占比超过 95%
    if (heapStatus.isOverMaxThreshold) {
      // 已达到最大阀值,强制触发 trigger,防止后续出现大内存分配导致 OOM 进程 Crash,无法触发 trigger
      currentTimes = 0;
      return true;
    }
  // ③、当前使用内存是否达到触发条件,内存使用占比超过 80、85、90
    if (heapStatus.isOverThreshold) {
      // 默认是 true
      if (heapThreshold.ascending()) {
         // ④、此时记录的内存占用比上次记录的高、达到最大阈值
        if (lastHeapStatus == null || heapStatus.used >= lastHeapStatus.used || heapStatus.isOverMaxThreshold) {
          currentTimes++;
        } else {
          currentTimes = 0;
        }
      } else {
        currentTimes++;
      }
    } else {
      currentTimes = 0;
    }
    // 将本地记录进行缓存
    lastHeapStatus = heapStatus;
    // ⑤、记录的次数超过 3 次,则触发条件
    return currentTimes >= heapThreshold.overTimes();
  }
  private HeapStatus lastHeapStatus;
  private HeapStatus currentHeapStatus() {
    HeapStatus heapStatus = new HeapStatus();
    heapStatus.max = Runtime.getRuntime().maxMemory();
    heapStatus.used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    float heapInPercent = 100.0f * heapStatus.used / heapStatus.max;
    heapStatus.isOverThreshold = heapInPercent > heapThreshold.value();
    heapStatus.isOverMaxThreshold = heapInPercent > heapThreshold.maxValue();
    return heapStatus;
  }
复制代码


解释:


  • ①:currentHeapStatus 方法是获取当前的内存状态,主要收集了当前最大内存、已使用的内存、已使用内存的占比、已使用的内存占比是否超过阈值,已使用的内存占比是否超过最大阈值。
  • ②:当前已使用内存是否达到最大阈值,内存使用占比超过 95%(常量值,可配置),如果超过的话,则直接触发
  • ③:当前已使用内存占比是否触发到阈值,该阈值会根据机型内存来进行变更,具体看 KConstants.getDefaultPercentRation(常量值,可配置)
  • ④、如果本次记录的内存占比比上次记录的还要大,或是触发到了最大阈值,则记录一下次数
  • ⑤:记录的次数超过 3 次,则触发

对于第四点我开始是有点疑虑的,只有内存是在连续 3 次增长的时候才会迭代次数,并且我们的检测是轮训的 5s,如果在增长的次数刚好 2 次,gc 回收又让内存重新回跌,然后次数又会被重置,下次再又增长上来,又要从 0 开始记录次数,这种会不会漏检?但又思考再三,如果内存泄漏的话,内存的趋势肯定是增长状态的,只不过是时间问题,他并不像 crash 检测那样,需要很高的时效性。


2、dump 内存堆栈


Dump hprof是通过虚拟机提供的 API dumpHprofData 实现的,这个过程会 “冻结” 整个应用进程,造成数秒甚至数十秒内用户无法操作,这也是LeakCanary 无法线上部署的最主要原因,如果能将这一过程优化至用户无感知,将会给 OOM 治理带来很大的想象空间。


正如 KOOM 所说的,解决 dump 无感知会是非常大的想象空间,因为他可以部署到线上监控。


KOOM 使用 fork dump 操作,从当前主进程 fork 出一个子进程,由于 linux 的 copy-on-write 机制,子进程和父进程共享的是一块内存,那么我们就可以在子进程中进行 dump 堆栈,不影响主进程的运行。当然其中还是有很多的坑,这里不展开讲,可以查看快手的文章 解决 Dump hprof 冻结 app 部分


HeapDumpTrigger.class

public void doHeapDump(TriggerReason.DumpReason reason) {
    // 生成 dump 的 hprof 文件存储路径
    KHeapFile.getKHeapFile().buildFiles();
    ...
    // 开始 dump
    boolean res = heapDumper.dump(KHeapFile.getKHeapFile().hprof.path);
    ...
  }
复制代码


heapDumper 实现类有三个,我们只看 ForkJvmHeapDumper 类


ForkJvmHeapDumper

@Override
  public boolean dump(String path) {
    ...
    // 适配 Android 11 ,和下面流程差不多
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
      return dumpHprofDataNative(path);
    }
    ...
    try {
      // ①、挂起主进程并 for 出子进程
      int pid = trySuspendVMThenFork();
      if (pid == 0) {
        // ②、子进程开始 dump hprof
        Debug.dumpHprofData(path);
        // 结束子进程
        exitProcess();
      } else {
        // ③、恢复挂起的主进程
        resumeVM();
        // ④、等待子进程的 dump
        dumpRes = waitDumping(pid);
      }
    } catch (IOException e) {
       e.printStackTrace();
    }
    return dumpRes;
  }
复制代码


解释:


  • ① : 调用 native 方法,挂起当前的主进程,并 for 出子进程,该挂起仅仅只是更改 ThreadList  变量的线程状态味 suspend,主要目的的欺骗子进程的 dump
  • ② : 子进程开始 dump hprof 文件
  • ③ : 恢复挂起的主进程,也是更改 ThreadList  变量状态
  • ④ : 等待子进程退出, 看到 issue #81 有人对这个等待过程提出了疑虑,作者也进行相应的解答,waitPid 只是暂停线程,而我们 dump 的过程是在 HandlerThread 进行的,所以并不影响主线程

dump 出的堆栈已存放到了指定 path 中,接下来只需要继续回到 doHeapDump 方法,做接下来的解析操作。


3、分析 hprof 文件


分析的回调有点长,就直接写类和方法好了:

  • KOOMInternal.onHeapDumped
  • HeapAnalysisTrigger.startTrack
  • HeapAnalysisTrigger.trigger
  • HeapAnalysisTrigger.doAnalysis
  • HeapAnalyzeService.runAnalysis : 启动一个 IntentService 服务
  • HeapAnalyzeService.doAnalyze
  • KHeapAnalyzer.analyze


KHeapAnalyzer.class

public boolean analyze() {
    // 查找泄漏的引用链
    Pair<List<ApplicationLeak>, List<LibraryLeak>> leaks = leaksFinder.find();
    if (leaks == null) {
      return false;
    }
    //将 gc 引用链写入到 report 文件中
    HeapAnalyzeReporter.addGCPath(leaks, leaksFinder.leakReasonTable);
    // 标记当前 report 已完成
    HeapAnalyzeReporter.done();
    return true;
  }
复制代码


对于解析,KOOM 做了如下优化:


  • GC root  剪枝,由于我们搜索 Path to GC Root 时,是从 GC Root 自顶向下 BFS,如JavaFrameMonitorUsed等此类 GC Root 可以直接剪枝。
  • 基本类型、基本类型数组不搜索、不解析。
  • 同类对象超过阈值时不再搜索。
  • 增加预处理,缓存每个类的所有递归 super class,减少重复计算。
  • 将object ID的类型从long修改为int,Android虚拟机的object ID大小只有32位,目前shark里使用的都是long来存储的,OOM时百万级对象的情况下,可以节省10M内存。

4、总结


KOOM 将内存泄漏做到线上监控,已经是市面上几款内存泄漏框架中的一种创新了

目录
相关文章
|
存储 JSON 监控
APM监控 · 入门篇 · Android端测监控平台建设(1)
APM 全称 Application Performance Management & Monitoring (应用性能管理/监控) 性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿、响应速度慢、发热严重、流量电量消耗大等问题的时候,很可能就会卸载掉我们的 App。这也是我们在目前工作中面临的巨大挑战之一,尤其是低端机型。
2956 0
APM监控 · 入门篇 · Android端测监控平台建设(1)
|
监控 调度 Android开发
看完这篇 Android ANR 分析,就可以和面试官装逼了!
ANR概述 首先,ANR(Application Not responding)是指应用程序未响应,Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR。
7966 0
|
NoSQL 安全 Java
分布式锁实现原理与最佳实践
在单体的应用开发场景中涉及并发同步时,大家往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。接下来本文将为大家分享分布式锁的最佳实践。
|
7月前
|
消息中间件 弹性计算 安全
使用计算巢打造纯内网部署的PaaS服务
阿里云计算巢中,私有化部署的PaaS软件常开启公网IP,但这样做不符合最佳实践,尤其是对数据库这类核心数据存储。公网访问增加了安全风险和管理成本。计算巢为此推出了内网WEB服务安全代理,解决纯内网部署难题。该代理提供与公网一致的体验,消除安全风险,无需公网费用。`
|
存储 算法 Java
【内存】Android C/C++ 内存泄漏分析 unreachable
【内存】Android C/C++ 内存泄漏分析 unreachable
703 0
|
7月前
|
JavaScript 计算机视觉
vue使用tracking-min.js和face-min.js进行人脸识别
vue使用tracking-min.js和face-min.js进行人脸识别
415 0
|
7月前
|
存储 架构师 Linux
内存泄漏专题(7)hook之宏定义
内存泄漏专题(7)hook之宏定义
86 0
|
7月前
|
缓存 架构师 算法
Java内存溢出如何解决,Java oom排查方法,解决办法
在Java开发过程中,有效的内存管理是保证应用程序稳定性和性能的关键。不正确的内存使用可能导致内存泄露甚至是致命的OutOfMemoryError(OOM)。
|
Android开发
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突(一)
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突
|
存储 XML 监控
启动优化 · 基础论 · 浅析Android启动优化
启动优化 · 基础论 · 浅析Android启动优化
551 0
启动优化 · 基础论 · 浅析Android启动优化