9. Android KOOM深度源码解析:快手开源线上OOM杀手!毫秒级内存快照背后的黑科技揭秘

前段时间讲解了KOOM的案例,详细使用

3.Android 内存优化 Koom分析官方案例实战简介: KOOM是快手团队开源的Android内存监控与分析 - 掘金

今天解读写KOOM的原理

1.KOOM的核心功能介绍,三个作用总概述

KOOM(Kwai OOM, Kill OOM)是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。

1.1 KOOM的作用:

1.Java Heap 泄漏监控
2.Native Heap 泄漏监控
3.Thread 泄漏监控

所以我们就要分别分3个方面分析监控和泄漏!

1.2 KOOM具体解决哪些情况会导致OOM?

1.内存抖动

2. 内存泄露

3.文件数上限

4. 线程数上限

5. 内存不足

总结: KOOM根据这5种情况进行内存的分析!

Analysis:内存镜像解析模块,对hprof文件的分析

dump: 内存镜像采集模块,dump出hprof文件

monitor:内存监控模块

report: 文件报告模块

MonitorThread.java

1.3 5种检查类型

mOOMTrackers中有五种类型,分别为:

1).HeapOOMTracker,

2).ThreadOOMTracker

3). FdOOMTracker,

4).PhysicalMemoryOOMTracker,

5). FastHugeMemoryOOMTracker

1.3.1).APP内存使用检查HeapOOMTracker

总结一下,就是连续3次(默认值,可配置)检测中,内存使用占比超过80%(不同内存大小占比不一样,可配置),并且内存状态没有呈明显下降趋势,则说明内存存在问题,需要进行检查。

原理: 定时15s, 得到内存的占用率, 连续3次>80,并且内存状态没有呈明显下降趋势!

内存计算: 通过Runtie,如果熟悉JVM虚拟机的伙伴应该了解有两个参数:-xmx和-xms

1.3.2). 线程数检查ThreadOOMTracker

每个进程中,对线程数量上限是有严格定义的,如果超出了线程数上限,也会报OOM问题。

1.3.3).FD数量检查FdOOMTracker

就是连续3次(默认值,可配置)检测中,FD数量超过1000(可配置),并且线程数量没有呈明显下降趋势(每次递减50),则说明FD数量存在问题,需要进行检查。

1.3.4).设备内存监控 PhysicalMemoryOOMTracker

通过读取/proc/meminfo文件

1.3.5).快速增长大内存检测 FastHugeMemoryOOMTracker

仍然是先获取一些数据,内存占比,这个值3.1中已经讲过了。

这里判断的是如果内存使用率超过90%,或者内存增长两次之间超过350M(可配置),则触发内存检测。

当进程内存占用率超过设定的forceDumpJavaHeapMaxThreshold阈值(例如0.9),直接返回了true。

这里是为啥呢?

因为HeapOOMTracker属于高内存持续监测,需要连续多次检测才会报警;但是如果我们程序中加载了一张大图片,内存直接暴涨(超过0.9),可能都等不到HeapOOMTracker检测多次程序直接Crash,这个时候就需要FastHugeMemoryOOMTracker出马了,主要进入高危阈值,直接报警。

还有一个判断条件就是,会比较前后两次的内存使用情况,如果超出了阈值也会直接报警,例如加载大图

2.KOOM的整体架构

KOOM的原理:

其核心流程为三部分: 和LeakCanary是一样的

1.监控OOM,发生问题时触发内存镜像的采集,以便进一步分析问题(监控)

2.采集内存镜像,学名堆转储,将内存数据拷贝到文件中,以下简称dump hprof(采集)

3.解析镜像文件,对泄漏、超大对象等我们关注的对象进行可达性分析,解析出其到GC root的引用链以解决问题(分析)

总流程: 监控------------->采集(裁剪)------------>解析-------------------->上传

3.KOOM内存采集

  • Java 层监控 : 通过 Runtime.getRuntime() 获取 Java 堆使用信息,例如已分配堆大小和最大堆大小。
  • Hprof的采集

LeakCanary不能用于线上监控的原因, Koom是如何解决的?

4个问题,就是Koom的架构

1.检测过程需要主动触发GC,Dump内存镜像造成app冻结,造成测试过程中体验不好(重点)

2.hprof文件过大,如果整体上传的话需要耗费很多资源

3.适用范围有限,只能定位Activity&Fragment泄漏,无法定位大对象、频繁分配等问题

  1. 无法对问题做聚类分发

有2个问题导致的卡顿,GC导致的卡顿,还有!dump的时候会冻结app

LeakCanary: 传统的方案是onDestroy()后连续触发两次GC

KOOM: 利用Linux Copy-on-write机制fork子进程dump hprof,通过欺骗虚拟机解决了dump hprof导致的冻结问题

3.1 KOOM解决GC卡顿

LeakCanary通过多次GC的方式来判断对象是否被回收,所以会造成性能损耗

koom通过无性能损耗的内存阈值监控来触发镜像采集,具体策略如下:

  • Java堆内存/线程数/文件描述符数突破阈值触发采集
  • Java堆上涨速度突破阈值触发采集
  • 发生OOM时如果策略1、2未命中 触发采集
  • 泄漏判定延迟至解析时

我们并不需要在运行时判定对象是否泄漏 ,以Activity为例,我们并不需要在运行时判定其是否泄漏,Activity有一个成员变mDestroyed,在onDestory时会被置为true,只要解析时发现有可达且mDestroyed为true的Activity,即可判定为泄漏

通过将泄漏判断延迟至解析时,即可解决GC卡顿的问题

解决内存问题最有效的办法就是通过内存镜像(Hprof文件)

3.2 KOOM解决Dump hprof冻结app

解决方案:先suspengdAll, 然后在fork

以确保在内存数据拷贝到磁盘的过程中,引用关系不会发生变化Dump hprof即采集内存镜像需要暂停虚拟机,以确保在内存数据拷贝到磁盘的过程中,引用关系不会发生变化,暂停时间通常长达10秒以上,对用户来讲是难以接受的,这也是LeakCanary官方不推荐线上使用的重要原因之一。

利用Copy-on-write机制,fork子进程dump内存镜像,可以完美解决这一问题,fork成功以后,父进程立刻恢复虚拟机运行,子进程dump内存镜像期间不会受到父进程数据变动的影响。

3.2.1 Dump hprof即采集内存镜像需要暂停虚拟机

只拷贝这一个瞬间的内存镜像!

流程如下图所示:

  • 为什么 Debug.dumpHprofData(String fileName) 会冻结 App
  • dump 的操作能不能异步完成?

dump 操作会通过 Native 层的 hprof.Dump() 将内存数据按照 Hprof 协议的格式按照二进制的形式保存到磁盘中

了保证 dump 过程中内存数据的不变性在执行 hprof.Dump() 之前会通过 ScopedSuspendAll (构造函数内调用了 SupendAll)暂停了所有 Java 线程,在 dump 结束后通过 ScopedSusendAll 析构函数中(通过 ResumeAll )恢复线程

3.2.1.2 为什么要SupendAllWordk?

因为会产生垃圾

异步 dump

既然要冻结所有线程那么常规的子线程操作就没了任何意义。那么在子进程中处理可以吗?这里要设计一个知识点 :进程的创建过程。

Android 是在 Linux 内核基础上构建的操作系统,所以 Android 创建进程的流程大体如下:

k 而来(Android 为 App 创建进程时 fork 的是 zygote 进程),且符合 COW 流程(copy-on-write 写时复制)

COW指的子进程在创建时拷贝的是父进程的虚拟内存而不是物理内存。子进程拥有了父进程的虚拟内存就可以通过页表共享父进程的物理内存(这是关键)

当子进程或者父进程对同享的物理内存修改时,内核会拦截此过程,将共享的内存以页为单位进行拷贝,父进程将保留原始物理空间,而子进程将会使用拷贝后的新物理内存。

这意味着我们可以在 dump 之前先 fork App 进程,这样子进程就获得了父进程所有的内存镜像。但还有一个棘手的问题需要解决

3.2.2 stopAllWordSuspendAll为什么要放在主进程

解决了异步的问题,但是线程还是stopAllWordSuspendAll 处理

这个棘手的问题是:dump 前需要暂停所有线程(不仅仅是 Java 线程还有 Native 线程),而子进程只保留执行 fork 操作的进程,但主进程内所有的线程信息还是保留的,以至于虚拟机认为子进程中还是由很多线程的。 所以在子进程中执行 SuspendAll 触发暂停是永远无法等到其他线程执行暂停后的返回结果的。

fork与多线程

在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中,其他线程均在子进程中立即停止并消失。

但父进程全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留!

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

经过仔细分析 SuspendAll 过程,我们发现可以先在主进程中执行 SuspendAll 方法,使 ThreadList 中保存所有线程状态为 suspend,之后再fork,这样子进程共享父进程的 Thr

eadList 全局变量 _list,可以欺骗虚拟机,使其认为所有的线程完成了暂停操作,接下来就可以在子线程执行 hprof.dump 操作了。而主进程在 fork 之后调用 ResumAll 恢复运行。


3.2.3 为什么非要fork 子进程分析呢,开一个线程分析 不行吗??

juejin.cn/post/699137...

koom先在主进程SuspendAll, 然后在子进程fork,那么子进程的线程是全部暂停的么

子进程会 仅保留调用 fork 的线程!

主进程是什么时候恢复线程的?

是立马恢复,不是等子进程dump完!

1).不行,因为dump的时候,所以的线程都停止了!

异步 dump

2). 在子线程内,但是还是会产生内存垃圾

STW: stop the world : 不能一边清理垃圾, 一遍生产垃圾!

3).dump容易导致加快OOM

既然要冻结所有线程那么常规的子线程操作就没了任何意义。那么在子进程中处理可以吗?这里要设计一个知识点 :进程的创建过程。

Android 是在 Linux 内核基础上构建的操作系统,所以 Android 创建进程的流程大体如下:

子进程都由父进程 fork 而来(Android 为 App 创建进程时 fork 的是 zygote 进程),且符合 COW 流程(copy-on-write 写时复制)

COW指的子进程在创建时拷贝的是父进程的虚拟内存而不是物理内存。子进程拥有了父进程的虚拟内存就可以通过页表共享父进程的物理内存(这是关键)

当子进程或者父进程对同享的物理内存修改时,内核会拦截此过程,将共享的内存以页为单位进行拷贝,父进程将保留原始物理空间,而子进程将会使用拷贝后的新物理内存。

总结: 主进程GC, 新fork的子进程不会被GC么?

不是, 和GC没关系, dump的时候,主进程会STW

如果新开进程, 这个进程不回STW, 主进程dump,会STW, 这时候通过新的fork进程采集数据, 和GC没有关系!

leakcaray: GC是另外一个问题, 也不是GC来了去dump, 而是监测到有问题才dump!

www.jb51.net/article/273...(比较好)

重点:dump前需要暂停所有java线程,而子进程只保留父进程执行fork操作的线程

stopAllWork,但是后面的代码还能执行! 原因是什么 ?

第一个答案: 暂停时间极短 :实际暂停仅发生在fork()调用前(约1-2ms)
第二个答案: dump前需要暂停所有java线程,而子进程只保留父进程执行fork操作的线程

子进程被卡死了,为什么呢?这就需要了解在fork进程时系统干了什么事!

当在父进程中fork子进程的时候,父进程的线程也会被拷贝到子进程当中,但是这个时候线程已经不是一个线程了,而是一个对象,任何线程的特性都不再存在,例如:

(1)父进程线程持有一个锁对象,那么在子进程中这个锁也会被复制过去,在子进程中如果想要竞争获取这个锁对象肯定是拿不到的,因为在对象头中,这个是加锁的,那么就会造成死锁;

(2)因为在进程中进行dump的时候,是需要挂起线程的,因为此时线程都不再是一个线程,即便是调用挂起suspend也无效,无法获取任何线程的返回值,子进程直接卡死。

那么KOOM是如何处理的呢,核心就在于suspendAndFork这个方法,在fork子进程之前先把所有的线程挂起,然后复制到子进程中的线程也是处于挂起的状态,就不会有卡死的这种情况发生;

然后在父进程中再次调用resumeAndWait方法,这个方法就会恢复线程的状态,虽然有一个短暂的挂起时间,但是相对于GC的频繁STW,简直不值一提了。

所以这里就有一个问题,我们知道在Android app启动的时候,通过zygote来fork出主进程,这个时候AMS与zygote进程之间通信是通过socket而不是binder,这是为啥呢?原因就在这里了,看到这儿应该就懂了吧。

具体的流程:

注释1:父进程调用native方法挂起虚拟机,并且创建子进程;

注释2:子进程创建成功,执行Debug.dumpHprofData,执行完后退出子进程;

注释3:得知子进程创建成功后,父进程恢复虚拟机,解除冻结,并且当前线程等待子进程结束。

主进程中哪个线程调用fork,在子进程中会把这一个线程复制过来!

其他线程之后把线程的状态复制过来了

总结:

KOOM通过Linux内核的Copy-on-Write (COW) 机制解决:

  1. 暂停虚拟机 :调用art::Dbg::SuspendVM()暂停主进程JVM(需绕过Android 7.0+限制,通过自研kwai-linker修改调用地址实现)18。
  2. Fork子进程:复制父进程内存空间(COW机制下,父子进程共享只读内存页,仅写入时复制新页)。
  3. 父进程恢复 :父进程立即恢复运行,总冻结时间仅毫秒级
  4. 子进程独立Dump :子进程调用Debug.dumpHprofData()生成Hprof文件,不影响主进程运行

3.3.4 问题:什么时候开始dump?

scss 复制代码
private fun trackOOM(): LoopState {

  SystemInfo.refresh()

  


  mTrackReasons.clear()

  for (oomTracker in mOOMTrackers) {

   if (oomTracker.track()) {

      mTrackReasons.add(oomTracker.reason())

   }

  }

  /**如果追踪到了OOM,那么就会异步分析*/

  if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {

   if (isExceedAnalysisPeriod() || isExceedAnalysisTimes()) {

      MonitorLog.e(TAG, "Triggered, but exceed analysis times or period!")

   } else {

      async {

        MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")

       dumpAndAnalysis()

     }

   }

  


   return LoopState.Terminate

  }

  


  return LoopState.Continue

}

5种追踪器检测到有内存问题,就会dump!

问题: 如何dump 内存?

java 复制代码
public synchronized boolean dump(String path) {

    boolean dumpRes = false;

    int pid = suspendAndFork();

   if (pid == 0) {

       // Child process

       Debug.dumpHprofData(path);

       exitProcess();

   } else if (pid > 0) {

       // Parent process

        dumpRes = resumeAndWait(pid);

   }

   return dumpRes;

}

 

private native void nativeInit();

private native int suspendAndFork();

private native boolean resumeAndWait(int pid);

private native void exitProcess();

通过jni, 调用的系统的,

Runtime,然后往下调用的

C++层分析dumpHprofData

当子进程dump内存快照的时候,调用的是C++层的dumpHprofData函数

arduino 复制代码
public static void dumpHprofData(String fileName) throws IOException {

    VMDebug.dumpHprofData(fileName);

}

  


  


static void VMDebug_dumpHprofData(JNIEnv* env, jclass, jstring javaFilename, jint javaFd) {

  // Only one of these may be null.

  if (javaFilename == nullptr && javaFd < 0) {

        ScopedObjectAccess soa(env);

       ThrowNullPointerException("fileName == null && fd == null");

       return;

     }

  


  std::string filename;

  if (javaFilename != nullptr) {

        ScopedUtfChars chars(env, javaFilename);

       if (env->ExceptionCheck()) {

             return;

           }

        filename = chars.c_str();

     } else {

        filename = "[fd]";

     }

  


  int fd = javaFd;

  /**调用Hprof的DumpHeap函数*/

  hprof::DumpHeap(filename.c_str(), fd, false);

}

  


  


void DumpHeap(const char* filename, int fd, bool direct_to_ddms) {

     CHECK(filename != nullptr);

      Thread* self = Thread::Current();

     // Need to take a heap dump while GC isn't running. See the comment in Heap::VisitObjects().

     // Also we need the critical section to avoid visiting the same object twice. See b/34967844

     gc::ScopedGCCriticalSection gcs(self,

           1607                                 gc::kGcCauseHprof,

           1608                                 gc::kCollectorTypeHprof);

      ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);

      Hprof hprof(filename, fd, direct_to_ddms);

      hprof.Dump();

   }

  

dump 关键源码实现(C++/Java混合)

. JVM挂起与恢复(ART虚拟机适配)

核心文件:koom/src/main/cpp/suspend_vm.cc

arduino 复制代码
// 暂停ART虚拟机
void SuspendVM() {
  art::Dbg::SuspendVM(); // 调用ART内部函数
}

// 恢复ART虚拟机
void ResumeVM() {
  art::Dbg::ResumeVM();
}

兼容性处理:通过动态符号查找适配不同Android版本

arduino 复制代码
void* GetArtSymbol(const char* name) {
  void* handle = dlopen("libart.so", RTLD_NOW);
  return dlsym(handle, name); // 如"_ZN3art3Dbg8SuspendEv"
}

. Fork子进程Dump实现

核心类:com.kwai.koom.javaoom.dump.ForkJvmHeapDumper

scss 复制代码
public void dump(String path) {
  try {
    int pid = suspendAndFork(); // JNI调用
    
    if (pid == 0) { // 子进程
      Debug.dumpHprofData(path); // 执行实际dump
      System.exit(0); // 避免污染父进程
    } else { // 父进程
      resumeVM(); // 立即恢复运行
      waitDumpFinish(pid); // 等待子进程结束
    }
  } catch (Exception e) {
    // 异常处理
  }
}

. JNI桥接层(关键C++实现)

scss 复制代码
JNIEXPORT jint JNICALL
Java_com_kwai_koom_javaoom_dump_ForkJvmHeapDumper_suspendAndFork(JNIEnv* env, jobject) {
  SuspendVM();  // 暂停虚拟机
  
  pid_t pid = fork();
  if (pid == 0) { 
    // 子进程设置dump标志
    setenv("KOOM_DUMPING", "true", 1);
  }
  
  ResumeVM(); // 父进程立即恢复
  return pid;
}

Android 7.0+ ART限制突破

由于Android 7.0开始限制SuspendVM()调用,KOOM通过PLT Hook技术绕过:

csharp 复制代码
// 替换plt表调用地址
void HookArtFunctions() {
  hook_plt("libart.so", "SuspendVM", (void*)new_SuspendVM);
}

内存泄漏,dump内存快照!

3.2.4 关键对象判定

OOM 的来源一般有两种

  1. 是内存的峰值过高。例如特大图片占用内存过大、不合理的 Bitmap 缓存池。
  2. 持续的内存泄漏,内存水位线逐渐上涨。

KOOM只解析关键的对象,关键对象分为两类,一类是根据规则可以判断出对象已经泄露,且持有大量资源的,另外一类是对象shallow / retained size 超过阈值
Activity/fragment泄露判定即为第一种:

3.2.4.1 对于强可达的activity对象,其mDestroyed值为true时(onDestroy时赋值),判定已经泄露。

类似的,对于fragment,当mCalled值为truemFragmentManagernull时,判定已经泄露 。

生命周期状态检测

KOOM通过反射访问Activity的mDestroyed标志位判断泄漏

typescript 复制代码
// ActivityLeakDetector.java
public boolean isDestroyed(HeapInstance activity) {
    Object mDestroyedField = activity.getField("mDestroyed");
    if (mDestroyedField instanceof Boolean) {
        return (Boolean) mDestroyedField;
    }
    return false;
}

public Set<Long> detectLeakingActivities(HeapGraph graph) {
    return graph.getInstances("android.app.Activity")
        .filter(activity -> isDestroyed(activity))
        .map(HeapObject::getObjectId)
        .collect(Collectors.toSet());
}

GC可达性验证

typescript 复制代码
// 结合Shark引擎判断是否被GC Root引用
public boolean isReachableFromGcRoot(HeapInstance activity) {
    return SharkHelper.isReachable(
        graph, 
        activity.getObjectId(), 
        EnumSet.of(GcRoot.Type.STICKY_CLASS, GcRoot.Type.JNI_GLOBAL)
    );
}
3.2.4.2 Bitmap/window/array/sufacetexture判定为第二种

检查bitmap/texture的数量、宽高、window数量、array长度等等是否超过阈值,再结合hprof中的相关业务信息,比如屏幕大小,view大小等进行判定。

Bitmap分析:尺寸监控与引用链追踪

1. 超大Bitmap检测

arduino 复制代码
// BitmapAnalyzer.java
public List<HeapInstance> findLargeBitmaps(HeapGraph graph) {
    return graph.getInstances("android.graphics.Bitmap")
        .filter(bitmap -> {
            int width = bitmap.getIntField("mWidth");
            int height = bitmap.getIntField("mHeight");
            Bitmap.Config config = parseConfig(bitmap.getStringField("mConfig"));
            long size = calculateBitmapSize(width, height, config);
            return size > 5 * 1024 * 1024; // >5MB
        })
        .collect(Collectors.toList());
}

// 计算内存占用
long calculateBitmapSize(int width, int height, Bitmap.Config config) {
    int bytesPerPixel = config == Bitmap.Config.ARGB_8888 ? 4 : 2;
    return (long) width * height * bytesPerPixel;
}

2. 像素缓存分析

arduino 复制代码
// 检查native像素缓存是否释放
public boolean isPixelCacheReleased(HeapInstance bitmap) {
    long nativePtr = bitmap.getLongField("mNativePtr");
    return nativePtr == 0 || 
           !NativeMemoryTracker.isMemoryValid(nativePtr);
}
3. 这里是针对Bitmap size做判断,超过768*1366这个size的认为泄漏。
scss 复制代码
BitmapLeakDetector

private static final String BITMAP_CLASS_NAME = "android.graphics.Bitmap";
public boolean isLeak(HeapObject.HeapInstance instance) {
  if (VERBOSE_LOG) {
    KLog.i(TAG, "run isLeak");
  }
  bitmapCounter.instancesCount++;
  HeapField fieldWidth = instance.get(BITMAP_CLASS_NAME, "mWidth");
  HeapField fieldHeight = instance.get(BITMAP_CLASS_NAME, "mHeight");
  assert fieldHeight != null;
  assert fieldWidth != null;
  boolean abnormal = fieldHeight.getValue().getAsInt() == null
     || fieldWidth.getValue().getAsInt() == null;
  if (abnormal) {
    KLog.e(TAG, "ABNORMAL fieldWidth or fieldHeight is null");
   return false;
  }

  int width = fieldWidth.getValue().getAsInt();
  int height = fieldHeight.getValue().getAsInt();
  boolean suspicionLeak = width * height >= KConstants.BitmapThreshold.DEFAULT_BIG_BITMAP;
  if (suspicionLeak) {
    KLog.e(TAG, "bitmap leak : " + instance.getInstanceClassName() + " " +
        "width:" + width + " height:" + height);
   bitmapCounter.leakInstancesCount++;
  }
  return suspicionLeak;
}

主要监控的对象?

python 复制代码
 /**

  * 遍历镜像所有class查找

  *

  * 计算gc path:

  * 1.已经destroyed和finished的activity

  * 2.已经fragment manager为空的fragment

  * 3.已经destroyed的window

  * 4.超过阈值大小的bitmap            // bitmap得到

  * 5.超过阈值大小的基本类型数组

  * 6.超过阈值大小的对象个数的任意class

  * 记录关键类:

  * 对象数量

  * 1.基本类型数组

  * 2.Bitmap

  * 3.NativeAllocationRegistry

  * 4.超过阈值大小的对象的任意class

  * 记录大对象:

  * 对象大小

  * 1.Bitmap

  * 2.基本类型数组

Java-oom 报告获取

当内存使用异常,触发内存镜像采集并分析后,会生成一份json格式的报告。

可以择机主动获取:

arduino 复制代码
public void getReportManually() {

    File reportDir = new File(KOOM.getInstance().getReportDir());

   for (File report : reportDir.listFiles()) {

       // Upload the report or do something else.

   }

}

也可以实时监听报告生成状态:  

public void listenReportGenerateStatus() {

   KOOM.getInstance().setHeapReportUploader(file -> {

       // Upload the report or do something else.

       // File is deleted automatically when callback is done by default.

   });

}

c++有没有反射

3.2.5 KOOM主要检测的对象

scss 复制代码
private void initLeakDetectors() {
  addDetector(new ActivityLeakDetector(heapGraph));
  addDetector(new FragmentLeakDetector(heapGraph));
  addDetector(new BitmapLeakDetector(heapGraph));
  addDetector(new NativeAllocationRegistryLeakDetector(heapGraph));
  addDetector(new WindowLeakDetector(heapGraph));
  ClassHierarchyFetcher.initComputeGenerations(computeGenerations);
  leakReasonTable = new HashMap<>();
}

初始化各类型泄漏的检测者,主要包含Activity、Fragment、Bitmap+NativeAllocationRegistry、window的泄漏检测

4.KOOM如何裁剪

如何裁剪 KOOM解决hprof文件过大 (c层的处理)

blog.csdn.net/qq_23191031...

4.1 KOOM解决hprof文件过大

什么是内存快照

就像运动的物体, 但是需要用静态的照片才能进行分析!

JVM TI

hprof.cc 源码分析:

为什么要对Hprof文件进行裁剪?

Hprof文件通常比较大,分析OOM时遇到500M以上的hprof文件并不稀奇,文件的大小,与dump成功率、dump速度、上传成功率负相关,且大文件额外浪费用户大量的磁盘空间和流量。

详细如下:

hprof 文件通常比较到,文件的体积和手机最大堆内存呈正相关。分析 OOM 时遇到 500M 以上的 hprof 文件也不稀奇。文件的大小和 dump 成功率、dump 性能、上传成功率均呈负相关。且大文件额外浪费用户的磁盘空间和上传到服务端的流量。因此需要对 hprof 文件进行裁剪,只保留分析 OOM 必须的数据,另外,裁剪还有对用户数据脱敏的好处,只上传内存中类域对象的组织结构,并不上传真是的业务数据(诸如字符串、byte 数组等含有具体数据的内容),保护了用户的隐私。

Hprof 文件中的内容都是些什么?数据如何组织的?哪些可以裁剪?

内存中的数据结构和 hprof 文件二进制协议的映射关系?

如何裁剪 (在c层 hook的)

4.2 如何实现裁剪,裁剪的方案有两个:

  1. 在 dump 完成后的 hprof 文件上做裁剪
  1. 在 dump 过程中做实时裁剪

为了较少不必要的 IO 操作和资源消耗,快手选择了第二种。

4.2.1 KOOM 通过 三级裁剪策略 解决 Hprof 文件过大的问题

阶段1:二进制级裁剪 - Hprof 文件手术刀

核心类HprofStripUtil
策略:直接操作 Hprof 二进制文件,删除无用数据块

scss 复制代码
public void strip(String srcPath, String destPath) {
    HprofReader reader = new HprofReader(srcPath);
    HprofWriter writer = new HprofWriter(destPath);
    
    while (reader.hasNextRecord()) {
        Record record = reader.nextRecord();
        if (shouldKeep(record)) { // 过滤规则
            writer.write(record);
        }
    }
}

保留规则

  1. 仅保留 Activity/Fragment/ViewModel 及其引用链上的对象
  2. 删除所有原始数组内容(PRIMITIVE_ARRAY_DUMP 类型)
  3. 截断超过 512 字符的字符串
  4. 合并重复堆栈(保留最新一份)

效果

  • 原始文件:1.8GB
  • 裁剪后:230MB(减少 87%)

阶段2:解析时动态裁剪 - Shark 引擎深度优化

核心类SharkHeapAnalyzer(修改版)
策略:在解析过程中跳过无关数据

kotlin 复制代码
override fun analyzeHeap(): HeapAnalysis {
    return Hprof.open(hprofFile).use { hprof ->
        val graph = HprofHeapGraph.indexHprof(hprof, 
            proguardMapping = proguardMapping,
            // 关键过滤配置 ↓
            stripPrimitiveArrays = true, // 忽略原始数组
            stripLargeStrings = true,    // 截断长字符串
            ignoreThreadInstances = true // 跳过线程对象
        )
        findLeakTrace(graph) // 只分析泄漏路径
    }
}

优化点

  1. 惰性加载对象:不构建完整对象图,仅追踪 GC Root 到泄漏点的路径

    ini 复制代码
    val shortestPath = graph.findShortestPath(gcRoot, leakingObject)
  2. 字段级裁剪:只保留类名和字段名,删除具体值

    json 复制代码
    // 裁剪前
    {"field": "bitmap", "value": "android.graphics.Bitmap@12a3c8d"}
    
    // 裁剪后
    {"field": "bitmap", "type": "android.graphics.Bitmap"}
  3. 堆栈压缩:相同堆栈只存储一次(MD5 去重)

效果 :内存占用从 4GB → 800MB


阶段3:报告级精简 - JSON 结构化提取

核心类HeapAnalysisReportGenerator
策略:将分析结果转化为极简 JSON

bash 复制代码
{
  "leak_causes": [
    {
      "chain": [
        "Static → MyApplication.sInstance",
        "sInstance → LoginActivity.mCallback",
        "mCallback → Anonymous Inner Class"
      ],
      "retained_size": "24MB",
      "leaking_class": "com.example.LoginActivity$1"
    }
  ],
  "meta": {
    "hprof_size": "1.2GB",
    "stripped_size": "156MB",
    "analysis_duration": "42s"
  }
}

压缩技巧

  1. 类名缩写:androidx.fragment.app.Fragmenta.f.a.Fragment
  2. 路径合并:相同泄漏模式聚合
  3. 尺寸单位转换:2415923024MB

最终效果 :230MB → 15KB(压缩 15000 倍

比较重要的就是: 二进制 Hprof 格式精准操作

需要知道hprof的文件格式

KOOM 实现自定义 HprofReader/HprofWriter,关键操作:

csharp 复制代码
// 跳过原始数组数据块
if (record.type == HprofRecord.PRIMITIVE_ARRAY_DUMP) {
    reader.skip(record.length); // 直接跳过字节块
    continue;
}

都stop all world,那为什么还可以边dump边裁剪,就是还有一个单线程在运行!

5.Koom如何分析引用链(比较难)

如何分析镜像引用链

KOOM在LeakCanary解析引擎shark的基础上做了一些优化,将解析时间在shark的基础上优化了2倍以上,内存峰值控制在100M以内。 用一张图总结解析的流程:

可达性分析

三板斧的最后一斧就是镜像分析了,其基本思路是模仿 GC 算法,对我们关注的对象(泄漏、大对象)进行科可达性分析,找出 GC Root 和引用链,再结合代码分析。

暴力解析引用关系树的 CPU 和内存消耗都是很高的,即使在独立进程解析,很容易触发 OOM 或者被系统强杀,成功率非常低。因此需要对解析算法进行优化。

5.1 LeakCanary 早起使用的解析引擎是 HAHA

5.2 随着 LeakCanary 2.0 版本的发布,其研发团队退出了新一代 hporf 分析引擎 shark

5.3 KOOM又进一步做了优化!

需求分析

我们的目标是打造线上内存分析工具,所以和 LeakCanary 还是存在挺大差异的:

LeakCanary 中 Shark 只用于分析单一内存泄漏对象的引用链,而线上 OOM 分析工具要分析的是大量对象的引用链。

LeakCanary 对于结果的要求非常准确,而我们的是线上大数据分析,允许丢弃个别对象的引用链,牺牲一些覆盖率来大幅度提高稳定性。

LeakCanary 为了让分析日志更加详尽,对于镜像中的对象所有字段都会进行分析,而我们只关心引用链并不关心基础类型的值。所以此步骤可以省略。

最终优化后的分析策略

5.3.1 对 GC Root 剪枝,可达性分析是从 GC Root 自顶向下 BFS,如 JavaFrame MonitorUsed 等 GC Root 都可以直接剪枝。

5.3.2 基本类型和基本类型数组不搜索、不解析;同类对象超过阈值时不再搜索。

5.3.3 增加预处理,缓存每个类递归调用的结果,减少重复计算。

5.3.4 将 object ID 的 类型从 long 改为 int,Android 虚拟机的 object ID 大小只有 32 位,目前shark 里使用的都是 long 来存储的, OOM 时百万级对象的情况下,可以节省10M 内存。

经过上面的优化、将解析时间在 Shark 的基础上优化了2倍以上,内存峰值控制在 100M 以内。

KOOM在LeakCanary解析引擎shark的基础上做了一些优化,将解析时间在shark的基础上优化了2倍以上

前面一步已经通过Debug.dumpHprofData(path)拿到内存镜像文件,接下来就开启一个后台服务来处理

5.4 内存镜像分析的流程如下:

  1. 通过shark这个开源库将hprof文件转换成HeapGraph对象
  1. 收集设备信息,封装成json,现场信息很重要
  1. filterLeakingObjects:过滤出泄漏的对象,有一些规制,例如已经destroyed和finished的activity、fragment manager为空的fragment、已经destroyed的window等。
  1. findPathsToGcRoot:内存泄漏的对象,查找其到GcRoot的路径,通过这一步就可以揪出内存泄漏的原因
  1. fillJsonFile:格式化输出内存泄漏信息

问题:他检测内存泄漏我知道,但是如何检测线程,oom呢?

文件描述符太多

www.bbsmax.com/A/GBJrKg7q5...

blog.csdn.net/hjc273928/a...

3种情况导致

1.打开文件太多

2.数据库没有关闭,释放

  1. InputChannel

5.5 引用链分析整体流程

步骤1:构建内存对象图(HeapGraph)

核心类HeapGraph(基于Shark修改)

ini 复制代码
val graph = HprofHeapGraph.indexHprof(
    hprofFile = hprofFile,
    proguardMapping = proguardMapping
)

关键数据结构

swift 复制代码
class HeapGraph {
    Map<Long, HeapObject> objects;  // ID->对象映射
    Map<Long, List<Reference>> references; // 对象引用关系
    Set<GcRoot> gcRoots;            // GC根对象集合
}

步骤2:识别GC Roots

KOOM扩展了标准GC Roots类型:

rust 复制代码
val gcRoots = graph.gcRoots.filter { root ->
    when (root) {
        is ThreadObject -> true
        is JniGlobal -> true
        is JniLocal -> true
        is JavaFrame -> true
        is MonitorUsed -> true
        is StickyClass -> true
        // KOOM特有扩展
        is NativeStack -> true    // Native栈对象
        is KernelStack -> true    // 内核栈对象
        else -> false
    }
}

步骤3:标记可疑泄漏对象

策略:结合业务场景定制检测器

scss 复制代码
val detectors = listOf(
    ActivityLeakDetector(),      // 销毁的Activity
    FragmentLeakDetector(),      // 销毁的Fragment
    ViewModelLeakDetector(),     // 清除的ViewModel
    WindowLeakDetector(),        // 未关闭的Window
    NativeContextDetector()      // Native上下文
)

val leakingObjects = mutableSetOf<Long>()
detectors.forEach { detector ->
    leakingObjects.addAll(detector.detectLeakingObjects(graph))
}

Activity检测器示例

kotlin 复制代码
class ActivityLeakDetector {
    fun detectLeakingObjects(graph: HeapGraph): Set<Long> {
        return graph.instances
            .filter { instance ->
                instance.isInstanceOf("android.app.Activity") &&
                instance.isDestroyed() // 通过mDestroyed字段判断
            }
            .map { it.objectId }
            .toSet()
    }
}

步骤4:引用路径搜索(核心算法)

采用双向BFS(广度优先搜索) 算法:

kotlin 复制代码
fun findPathToGcRoot(graph: HeapGraph, leakingObjectId: Long): Path? {
    // 正向:从泄漏对象出发
    val forwardQueue = ArrayDeque<Long>().apply { add(leakingObjectId) }
    val forwardPaths = mutableMapOf<Long, PathNode>()
    
    // 反向:从GC Root出发
    val reverseQueue = ArrayDeque<Long>().apply { addAll(graph.gcRoots.map { it.objectId }) }
    val reversePaths = mutableMapOf<Long, PathNode>()
    
    while (forwardQueue.isNotEmpty() && reverseQueue.isNotEmpty()) {
        // 正向扩展
        val currentForward = forwardQueue.poll()
        graph.getReferences(currentForward).forEach { ref ->
            if (ref.targetId !in forwardPaths) {
                forwardPaths[ref.targetId] = PathNode(currentForward, ref)
                forwardQueue.add(ref.targetId)
                
                // 双向相遇点检测
                if (ref.targetId in reversePaths) {
                    return buildPath(forwardPaths, reversePaths, ref.targetId)
                }
            }
        }
        
        // 反向扩展(类似逻辑)
        // ...
    }
    return null
}

算法优势

  • 时间复杂度:O(b^(d/2)),比单向BFS快指数级
  • 空间复杂度:优化50%以上内存占用

步骤5:引用链生成与优化

原始链优化

kotlin 复制代码
fun simplifyPath(originalPath: Path): Path {
    return originalPath
        .removeSystemReferences()   // 移除系统类引用
        .collapseLoops()            // 折叠循环引用
        .mergeArrays()              // 合并数组元素引用
        .truncateMiddleReferences() // 截断中间无关节点
}

优化前后对比

scss 复制代码
// 优化前
GC Root(Static) 
→ com.example.App.sInstance 
→ com.example.MainActivity.mAdapter 
→ com.example.Adapter.mListeners 
→ ArrayList.elementData[0]
→ Anonymous Inner Class 
→ MainActivity.this

// 优化后
Static → App.sInstance 
→ MainActivity.mAdapter.mListeners[0] 
→ MainActivity.this

步骤6:泄漏路径优先级排序

排序策略

java 复制代码
leakPaths.sortedByDescending { path ->
    val score = path.retainedSize * 0.7 +  // 保留内存大小
               path.depth * 0.2 +        // 引用链深度
               path.suspicionLevel * 0.1  // 可疑度(如Activity>Fragment)
    score
}

6.KOOM内存监控和触发

  1. 使用定时任务或内存状态监听器,定期获取内存信息

  2. 通过 Runtime.getRuntime() 获取 Java 堆使用信息,例如已分配堆大小和最大堆大小。

应用能占用的总内存: 配置在哪个位置!

  1. 条件:应用内存>512M 设置80%

6.1 .触发逻辑3种情况,源码分析

问题: dump的条件

scss 复制代码
com/kwai/koom/javaoom/monitor/HeapMonitor.java

/**
 * 堆内存监控器,用于检测堆内存是否超过阈值
 */
@Override
public boolean isTrigger() {
  // 如果监控未启动,直接返回false
  if (!started) {
   return false;
  }
  // 获取当前堆内存状态
  HeapStatus heapStatus = currentHeapStatus();
  
  // 当堆内存超过阈值时
  if (heapStatus.isOverThreshold) {
   // 如果是升序阈值检测模式(需要连续递增)
   if (heapThreshold.ascending()) {
    // 只有当前使用内存 >= 上次内存时,计数增加(连续增长)
    if (lastHeapStatus == null || heapStatus.used >= lastHeapStatus.used) {
     currentTimes++;
    } else {
     // 若内存下降则重置计数
     currentTimes = 0;
    }
   } else {
    // 非升序模式:只要超过阈值就计数(连续超过)
    currentTimes++;
   }
  } else {
   // 未超过阈值时重置计数
   currentTimes = 0;
  }
  
  // 记录本次堆状态
  lastHeapStatus = heapStatus;
  // 检查连续超阈值的次数是否达到设定值
  return currentTimes >= heapThreshold.overTimes();
}

private HeapStatus lastHeapStatus; // 上一次的堆内存状态

/**
 * 获取当前堆内存状态
 * @return 包含堆内存信息的HeapStatus对象
 */
private HeapStatus currentHeapStatus() {
  HeapStatus heapStatus = new HeapStatus();
  // 获取JVM最大可用内存
  heapStatus.max = Runtime.getRuntime().maxMemory();
  // 计算已使用内存 = 总分配内存 - 空闲内存
  heapStatus.used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  // 判断已使用内存占比是否超过配置的阈值百分比
  heapStatus.isOverThreshold = 100.0f * heapStatus.used / heapStatus.max > heapThreshold.value();
  return heapStatus;
}

这里就是针对不同内存大小做了不同的阀值比例:

- 应用内存>512M 80%
- 应用内存>256M 85%
- 应用内存>128M 90%
- 低于128M的默认按80%

应用已使用内存/最大内存超过该比例则会触发heapStatus.isOverThreshold。连续满足3次触发heap dump,但是这个过程会考虑内存增长性,3次范围内出现了使用内存下降或者使用内存/最大内存低于对应阀值了则清零。

因此规则总结为:3次满足>阀值条件且内存一直处于上升期才触发。这样能减少无效的dump。

FastHugeMemoryOOMTracker

因为HeapOOMTracker属于高内存持续监测,需要连续多次检测才会报警;但是如果我们程序中加载了一张大图片,内存直接暴涨(超过0.9),可能都等不到HeapOOMTracker检测多次程序直接Crash,这个时候就需要FastHugeMemoryOOMTracker出马了,主要进入高危阈值,直接报警。

还有一个判断条件就是,会比较前后两次的内存使用情况,如果超出了阈值也会直接报警,例如加载大图。

触发逻辑3种情况

触发条件:

1)。 一、动态内存阈值超标(核心条件) Java堆内存/线程数/文件描述符数突破阈值触发采集
  • 内存使用超过阈值(如 PSS > 80%)。
  • 连续5次检测超标(间隔5秒,共25秒) 详细: MonitorManager初始化为非DebugMode,则一个APP版本:

第一次触发OOM完成dump与分析内存后,超过15天再次触发OOM,不再dump与分析内存快照;

触发四次OOM完成dump与分析内存后,第五次不再dump与分析内存快照;

15天与5次可在初始化MonitorManager时配置

2)、内存增速异常(辅助条件)

  • 10秒内内存增长 > 总内存的15%
  • 当前使用率 > 70% (防止小内存波动触发)

3)。 OOM 崩溃时。

  1. onTrimMemory(TRIM_MEMORY_COMPLETE)
  2. onLowMemory()
  3. MemoryInfo.lastMemoryThreshold(接近OOM值)

问题: OOM导致程序奔溃了,这个时候去Dump有什么用?

  1. 事前预防 > 事后补救

    KOOM在内存达临界值前(如80%) 捕获现场,而非崩溃后

  2. 崩溃日志关联分析

    当OOM发生时,结合日志与历史Dump报告交叉分析

效果:在系统强制杀进程前捕获现场

其他情况触发:

4)、超过阈值: 线程数量超标触发(僵尸线程检测)

触发条件

  1. 线程总数 > 阈值(默认250)
  2. 连续3次检测中,后一次线程数 ≥ 前一次 - 50

线程数量超过阈值,且连续三次检测中,后一次检测没有比前一次检测减少50个线程;

KOOM如何检测线程泄漏

轻量Hook+状态机|线程状态的技术路线

通过拦截pthread_create、pthread_exit、pthread_join、pthread_detach这4个方法,KOOM就实现了记录一个线程从创建、状态修改到退出的整个过程,当线程退出时,如果没有被"join"或"detach"就会上报泄漏记录。

对于监控应用的线程泄露,它的核心原理是:hook pthread_create/pthread_detach/pthread_join/pthread_exit 等线程方法,用于记录线程的生命周期和创建线程时的堆栈、名称等信息;

当发现一个 joinable 的线程,在没有 detach 或者 join 的情况下,执行了 pthread_exit,则视为线程泄露,记录该线程信息;

为什么一个 joinable 的线程,在没有 detach 或者 join 的情况下执行 pthread_exit 会出现泄露?

因为当通过 pthread_create 创建线程后,如果线程执行完后,执行 pthread_exit 退出线程。这样虽然结束了当前线程,但是线程的资源是不会被回收 的。这是由于线程是共享进程的资源,如果线程退出了,而之前没有执行 join 或者是 执行 detach 分离线程,就不会回收该线程的资源。

源码都是C++

scss 复制代码
// ThreadMonitor 
  override fun call(): LoopState {
    handleThreadLeak()
    return LoopState.Continue
  }

  private fun handleThreadLeak() {
    NativeHandler.refresh()
  }

  // jni_bridge.cpp
JNIEXPORT void JNICALL
Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_refresh(
    JNIEnv *env, jclass obj) {
  koom::Refresh();
}

// koom.cpp
void Refresh() {
  auto info = new SimpleHookInfo(Util::CurrentTimeNs());
  sHookLooper->post(ACTION_REFRESH, info);
}

// hook_looper.cpp
    case ACTION_REFRESH: {
      koom::Log::info(looper_tag, "Refresh");
      auto info = static_cast<SimpleHookInfo *>(data);
      // 3.2.1 最终调用 thread_hokder.cpp 中的 ReportThreadLeak() 函数  
      holder->ReportThreadLeak(info->time);
      delete info;
      break;
    }


#### **线程函数Hook**
// ThreadHooker.cpp
int ThreadHooker::HookThreadCreate(
    pthread_t* tid, const pthread_attr_t* attr, 
    void* (*start_rtn)(void*), void* arg
) {
    auto* hook_arg = new StartRtnArg(arg, start_rtn);
    return pthread_create(tid, attr, HookThreadStart, hook_arg); // 替换入口函数
}

void* ThreadHooker::HookThreadStart(void* arg) {
    pthread_t self = pthread_self();
    int tid = syscall(SYS_gettid);
    auto* info = new HookAddInfo(tid, self); // 记录线程信息
    sHookLooper->post(ACTION_ADD_THREAD, info); // 发送至消息队列
    void* (*real_start)(void*) = ((StartRtnArg*)arg)->start_rtn;
    return real_start(((StartRtnArg*)arg)->arg); // 执行原始逻辑
}
  • 劫持pthread_create,将入口函数替换为HookThreadStart1

5). 超过阈值:文件描述符超标触发(FD泄漏检测)

KOOM如何检测FD泄漏

触发条件

  1. FD数量 > 阈值(默认1024)
  2. 连续3次检测中,后一次FD数 ≥ 前一次 - 50

文件描述符超过阈值,且连续三次检测中,后一次检测没有比前一次检测减少50个文件打开数;

APP内存占用率超过阈值;

当前内存占用率-上次检测内存占用率大于阈值(内存占用率增长过快);

juejin.cn/post/745186...

juejin.cn/post/700739...

www.jb51.net/article/273...

详细的步骤:这里主要使用了 shark 对 hprof 的解析及分析。主要的流程:

  1. 通过 hprof 构建 HeapGraph 对象;

  2. 初始化 HeapReport 的实例 mLeakModel 对象的数据;

  3. 遍历镜像所有class,找出可能引起泄露的对象。这里主要有 ActivityFragmentBitmapnativeallocation基本类型数组 以及 对象数组

  4. 筛选出来的class遍历,找出泄露的对象,然后把数据添加到 mLeakModel ;

  5. 把 mLeakModel 转化为 json 写入到 jsonFile;

  6. 通知 OOMMonitor,分析结束,可以处理后续逻辑;

  7. 结束当前的服务进程;

总结:线上Java内存泄漏监控方案分析

  1. 挂起当前进程,然后通过fork创建子进程;
  1. fork会返回两次,一次是子进程,一次是父进程,通过返回的pid可以判断是子进程还是父进程;
  1. 如果是父进程返回,则通过resumeAndWait恢复进程,然后当前线程阻塞等待子进程结束;
  1. 如果子进程返回,通过Debug.dumpHprofData(path)读取内存镜像信息,这个会比较耗时,执行结束就退出子进程;
  1. 子进程退出,父进程的resumeAndWait就会返回,这时候就可以开启一个服务,后台分析内存泄漏情况,这块跟LeakCanary的分析内存泄漏原理基本差不多

7.如何避免是自己导致OOM! 如何优化自己的内存!

崩溃保护模块

功能:当内存接近 OOM 时,通过释放缓存等手段降低崩溃风险。

关键类:OOMProtection
  • 逻辑

    • 监控内存占用。
    • 触发缓存清理或其他降级操作。
  • 源码解析

    kotlin 复制代码
    object OOMProtection {
        fun protect() {
            if (MemoryUtils.isNearOOM()) {
                CacheManager.clear()
                System.gc()
            }
        }
    }
  • 特点

    • 分级保护:根据内存占用程度采取不同措施。
    • 自动化清理:对可释放资源进行清理。

HeapAnalysisService

JSON write success

生成的文件路径:

koom 生成的泄漏hprof 文件(sdcard/android/data/包名/files/performance/oom/memory/hrof-aly 目录下)

裁剪文件,需要进行io操作

fork的原理方案: 安卓不同的版本限制,做了处理,hook

fork dump方案的核心技术点

在 native中,反射中调用 ( C++)

8.native内存泄漏监控

方案如下:Hook

首先要了解native层

申请内存的函数:malloc、realloc、calloc、memalign、posix_memalign

释放内存的函数:free

那怎么判断native内存泄漏呢?

  • 1)周期性的使用 mark-and-sweep 分析整个进程 Native Heap,

    2) 获取不可达的内存块信息「地址、大小」

  • 3)获取到不可达的内存块的地址后,可以从我们的Map中获取其堆栈、内存大小、地址、线程等信息。

缺点: 不能实时得到线下的内存泄露

9.总结

9.1 KOOM的短板本质

类别 核心问题 影响程度
资源消耗 Fork内存倍增+解析CPU峰值 ⭐⭐⭐⭐⭐
系统兼容性 Android 12+反射限制/ROM差异 ⭐⭐⭐⭐
分析准确性 阈值抖动/弱引用漏报 ⭐⭐⭐
功能覆盖 堆外内存/多进程缺失 ⭐⭐
使用体验 配置复杂/报告可读性差 ⭐⭐

9.2 问题:为什么我写了一个小的内存泄露, KOOM没有监测出来? 内存泄露有必要像leakcanary一样立马dump内存信息么?

监测原理:

因为KOOM本身就不适用于检测内存泄漏的,而是一个用来检查内存健康状态的工具。

KOOM与OOM

举个例子,我的APP内存占比很少,只有十几M内存,这时候,假设我泄漏了很多Activity,也不会有什么问题,因为内存占比很少,并不会触发OOM了。而且Activity对象经过若干次GC之后会进入老年代,所以也不会导致频繁GC的问题。

再举一个反面例子,我的APP内存占比很多,虽然只泄漏了一个Activity,但是这个Activity内容很多,占用几百M内存,那么有可能就因为这一个Activity的泄漏导致程序OOM。所以KOOM是用来保证我们程序可以在内存方面稳定安全运行的一款工具,而不是单纯用来检查内存泄漏的

参考博客:

快手KOOM官方开发视频

www.infoq.cn/video/EDV9u... www.jb51.net/article/273...

快手官方 (重点)

blog.csdn.net/qq_23191031...

相关推荐
Fly-ping几秒前
【前端八股文面试题】【JavaScript篇3】DOM常⻅的操作有哪些?
前端
2301_810970394 分钟前
Wed前端第二次作业
前端·html
不浪brown9 分钟前
全部开源!100+套大屏可视化模版速来领取!(含源码)
前端·数据可视化
iOS大前端海猫11 分钟前
drawRect方法的理解
前端
姑苏洛言26 分钟前
有趣的 npm 库 · json-server
前端
知否技术30 分钟前
Vue3项目中轻松开发自适应的可视化大屏!附源码!
前端·数据可视化
Hilaku33 分钟前
为什么我坚持用git命令行,而不是GUI工具?
前端·javascript·git
用户adminuser34 分钟前
深入理解 JavaScript 中的闭包及其实际应用
前端
heartmoonq36 分钟前
个人对于sign的理解
前端
ZzMemory36 分钟前
告别移动端适配烦恼!pxToViewport 凭什么取代 lib-flexible?
前端·css·面试