Android进阶KOOM线上APM监控全面剖析

目录
  • 正文
  • 1 Leakcanary为什么不能用于线上
    • 1.1 Leakcanary原理简单剖析
    • 1.2 小结
  • 2 KOOM原理分析
    • 2.1 KOOM引入
    • 2.2 KOOM源码分析
      • 2.2.1 trackOOM方法分析
      • 2.2.2 HeapOOMTracker
      • 2.2.3 ThreadOOMTracker
      • 2.2.4 FastHugeMemoryOOMTracker
    • 2.3 dump为何不能放在子线程
      • 2.3.1 ForkJvmHeapDumper分析
      • 2.3.2 C++层分析dumpHprofData
    • 2.4 多线程场景下fork进程
  • 3 总结

正文

APM,全称是Application Performance Management,也就是应用性能管理,这与我们平时写的业务可能并不相关,但是却承载着App线上稳定的责任。当一款App发布到线上之后,不同的用户有不同场景,一旦App出现了问题,为了避免黑盒,找不到头绪,就需要APM出马了。

对于App的性能,像CPU、流量、电量、内存、crash、ANR,这些都会是监控的点,尤其是当App发生崩溃的时候,需要回捞到当前用户的日志加以分析,找到此问题崩溃的堆栈,完成修复。否则就像是大海捞针,根本不知道哪里发生了崩溃,查找问题可能就需要找一半天。

那么对于成熟的线上APM监控,我们可能使用过Bugly、火山、Leakcanary,但其中都会有缺陷,对于一些大公司一般都会考虑自研APM,监控的对象也无非上述这些指标,那么如果让我们自己做一套APM监控,该怎么出方案呢?

1 Leakcanary为什么不能用于线上

如果有做过APM监控的伙伴,对于Leakcanary就很熟悉了,这个是一个老派的内存监控组件,但是我们在使用的时候,通常都是采用debugImplementation的方式引入,在debug环境下使用,而不是线上,这是为什么呢?

这个还需要从Leakcanary的原理说起了。

1.1 Leakcanary原理简单剖析

对于Java的引用类型,大家应该都清楚:强软弱虚,接下来我们通过一个简单的示例,看下四种引用的特性,这里我主要是介绍一下弱引用

Object object = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
WeakReference<Object> weak = new WeakReference<Object>(object,referenceQueue);
Log.e("Test","弱引用 "+weak.get());
object = null;
System.gc();
Thread.sleep(1000);
Log.e("Test","弱引用 "+weak.get());
Log.e("Test","弱引用队列 "+referenceQueue.poll());
System.gc();
Thread.sleep(2000);
Log.e("Test","弱引用 "+weak.get());
Log.e("Test","弱引用队列 "+referenceQueue.poll());

在这里我们模拟了一次资源回收的GC操作,当一个对象被置成null之后,通过gc正常情况下是可以被回收的;这里我们需要关注的是一个ReferenceQueue引用队列,当一个对象被回收之后,就会被放在这个队列中,从而与弱引用对象产生关联。

2022-12-16 21:15:57.598 24678-24678/com.lay.mvi E/Test: 弱引用 java.lang.Object@2f8c602
2022-12-16 21:15:58.600 24678-24678/com.lay.mvi E/Test: 弱引用 java.lang.Object@2f8c602
2022-12-16 21:15:58.600 24678-24678/com.lay.mvi E/Test: 弱引用队列 null
2022-12-16 21:34:45.099 3152-3152/com.lay.mvi E/Test: 弱引用 null
2022-12-16 21:34:45.099 3152-3152/com.lay.mvi E/Test: 弱引用队列 java.lang.ref.WeakReference@7cd1b13

那么这个时候我们模拟一下内存泄漏

object Constant {
    private var any: Any? = null
    fun hold(any: Any?) {
        this.any = any
    }
}

这里有一个单例,在创建出一个Object对象之后,就持有这个引用,然后这个时候把这个对象置为空

ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
WeakReference<Object> weak = new WeakReference<Object>(mObject,referenceQueue);
Log.e("Test","弱引用 "+weak.get());
Constant.INSTANCE.hold(mObject);
mObject = null;
System.gc();
Thread.sleep(2000);
Log.e("Test","弱引用 "+weak.get());
Log.e("Test","弱引用队列 "+referenceQueue.poll());

我们会发现无论如何GC,这个引用都无法被回收,因此对于内存泄漏的检测,就可以使用弱引用配个引用队列来进行关联对象的检测。

2022-12-16 21:38:47.743 5772-5772/com.lay.mvi E/Test: 弱引用 java.lang.Object@2f8c602
2022-12-16 21:38:49.744 5772-5772/com.lay.mvi E/Test: 弱引用 java.lang.Object@2f8c602
2022-12-16 21:38:49.744 5772-5772/com.lay.mvi E/Test: 弱引用队列 null
2022-12-16 21:38:51.745 5772-5772/com.lay.mvi E/Test: 弱引用 java.lang.Object@2f8c602
2022-12-16 21:38:51.745 5772-5772/com.lay.mvi E/Test: 弱引用队列 null

而在Leakcanary中,就是采用这种方式进行内存泄漏的检测,但是为啥不能用于线上,伙伴们应该知道,当系统在GC的时候,是需要STW的。

当一个Activity被销毁之后,Leakcanary会在onDestory方法中进行2次GC(为啥要多次GC,其实是因为一次GC并不能保证对象被回收,可以通过上面的例子中看出),如果熟悉JVM的伙伴应该知道,只要涉及到GC,极大的概率会触发STW,那么这个时候就会卡顿,如果有使用过Leakcanary,就会经常感受到卡顿甚至测试伙伴过来告诉你有bug,好在Leakcanary检测到内存泄漏的时候会有一个全局动画,不然真不好解释了。

1.2 小结

对于Leakcanary不能应用于线上,从性能角度来说,前面我们已经介绍了,主要就是归结于线程会STW;除此之外,因为Leakcanary在发生内存泄漏的时候,需要dump内存快照,生成hprof文件。

如果我们在Android Studio上分析过内存问题,会发现dump的过程非常耗时,会有3-4s的时间,有时甚至会卡死,但放在应用程序中,3-4s的时间可能直接导致ANR,因为整个过程应用程序是无响应的,所以Leakcanary只适合在线下测试环境中分析内存问题,不适合带着上线。

2 KOOM原理分析

那么既然Leakcanary不能带到线上,那么针对线上问题该如何分析呢?bugly只能分析Crash或者ANR,所以快手团队针对这些问题,研发了KOOM线上内存监控组件。

在此之前我们思考几个问题:

(1)对于线上APM,它需要非常高的实时性吗?如果出现内存泄漏就一定要立刻dump内存快照吗?

(2)dump内存快照是否能够在子线程中执行,而不阻塞主线程;

(3)对于生成的hprof文件,是否可以进行裁剪,加快分析进程尽快定位出问题来。

所以针对以上几个问题,我们看下KOOM是如何做到的。

2.1 KOOM引入

首先我们需要引入koom的依赖。

def VERSION_NAME = '2.2.0'
implementation "com.kuaishou.koom:koom-native-leak-static:${VERSION_NAME}"
implementation "com.kuaishou.koom:koom-java-leak-static:${VERSION_NAME}"
implementation "com.kuaishou.koom:koom-thread-leak-static:${VERSION_NAME}"
implementation "com.kuaishou.koom:xhook-static:${VERSION_NAME}"

因为整个KOOM的源码都是Kotlin写的,所以接下来的源码分析都会是Kotlin为主,具体的使用如下,在初始化完成OOMMonitor,就调用startLoop方法开启内存检测。

val commonConfig = CommonConfig.Builder().build()
val oomMonitorConfig = OOMMonitorConfig.Builder().build()
OOMMonitor.init(commonConfig, oomMonitorConfig)
OOMMonitor.startLoop(clearQueue = true,postAtFront = true, delayMillis = 5000)

2.2 KOOM源码分析

首先我们先看一下startLoop方法,从这个方法名字中,我们大概就能猜到这个方法在干什么事,如果熟悉Handler源码的伙伴应该明白,这肯定是循环的意思,当执行startLoop方法的时候,就是开启一个死循环。

override fun startLoop(clearQueue: Boolean, postAtFront: Boolean, delayMillis: Long) {
  throwIfNotInitialized { return }
  /**要在主进程中开启*/
  if (!isMainProcess()) {
    return
  }
  MonitorLog.i(TAG, "startLoop()")
  if (mIsLoopStarted) {
    return
  }
  mIsLoopStarted = true
  super.startLoop(clearQueue, postAtFront, delayMillis)
  getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)
}

首先startLoop是要在主进程中开启,然后执行了父类方法的startLoop,那么我们跟进去看一下。

open fun startLoop(
    clearQueue: Boolean = true,
    postAtFront: Boolean = false,
    delayMillis: Long = 0L
) {
  if (clearQueue) getLoopHandler().removeCallbacks(mLoopRunnable)
  if (postAtFront) {
    getLoopHandler().postAtFrontOfQueue(mLoopRunnable)
  } else {
    getLoopHandler().postDelayed(mLoopRunnable, delayMillis)
  }
  mIsLoopStopped = false
}

我们可以看到,在父类的startLoop方法中,同样是使用Handler来进行延迟消息的发送,执行的就是这个mLoopRunnable。

private val mLoopRunnable = object : Runnable {
  override fun run() {
    /**进行内存泄漏、OOM检测*/
    if (call() == LoopState.Terminate) {
      return
    }
    if (mIsLoopStopped) {
      return
    }
    getLoopHandler().removeCallbacks(this)
    getLoopHandler().postDelayed(this, getLoopInterval())
  }
}

在这个对象中,有一个核心方法call,就是用来做OOM和内存泄漏的检测

override fun call(): LoopState {
  if (!sdkVersionMatch()) {
    return LoopState.Terminate
  }
  if (mHasDumped) {
    return LoopState.Terminate
  }
  return trackOOM()
}

2.2.1 trackOOM方法分析

在call方法中,其实做的一个核心任务就是trackOOM,我们看下这个方法中主要是干了什么

private fun trackOOM(): LoopState {
  SystemInfo.refresh()
  mTrackReasons.clear()
  for (oomTracker in mOOMTrackers) {
    if (oomTracker.track()) {
      mTrackReasons.add(oomTracker.reason())
    }
  }
  /**如果追踪到了OOM,那么就会异步分析*/
  if (mTrackReasons.isNotEmpty() &amp;&amp; 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
}

首先是遍历mOOMTrackers数组,我们看下这个数组是什么

private val mOOMTrackers = mutableListOf(
  HeapOOMTracker(), ThreadOOMTracker(), FdOOMTracker(),
  PhysicalMemoryOOMTracker(), FastHugeMemoryOOMTracker()
)

这个数组其实是一些OOMTracker的实现类,就是这里大家需要思考一个问题,什么情况下会发生OOM?这里我总结一下主要可能发生OOM的场景:

(1)堆内存溢出;这个是典型的OOM场景;

(2)没有连续的内存空间分配;这个主要是因为内存碎片过多(标记清除算法),导致即便内存够用,也会造成OOM;

(3)打开过多的文件;如果有碰到这个异常OOM:open to many file的伙伴,应该就知道了;

(4)虚拟内存空间不足;

(5)开启过多的线程;一般情况下,开启一个线程大概会分配500k的内存,如果开启线程过多同样会导致OOM

所以看到这个数组中每个Tracker的名字,就应该明白,KOOM就是从这几个方面入手,随时监控可能发生OOM的风险,并发出告警信息。

for (oomTracker in mOOMTrackers) {
  if (oomTracker.track()) {
    mTrackReasons.add(oomTracker.reason())
  }
}

回到trackOOM这个方法,我们看在遍历这个数组的过程中,每取出一个Tracker,都执行了它的track方法

abstract class OOMTracker : Monitor&lt;OOMMonitorConfig&gt;() {
  /**
   * @return true 表示追踪到oom、 false 表示没有追踪到oom
   */
  abstract fun track(): Boolean
  /**
   * 重置track状态
   */
  abstract fun reset()
  /**
   * @return 追踪到的oom的标识
   */
  abstract fun reason(): String
}

我们看下SDK中的注释,这个方法的带有返回值的,如果返回了true,那么就表示追踪到了OOM,如果返回了false,即代表没有发生OOM;

然后如果追踪到了OOM,那么就将追踪到OOM的标识reason()塞到mTrackReasons这个集合当中。后面就会判断,如果这个集合不为空,那么就会去异步dump内存快照并分析,而不去阻塞主线程。

所以看到这里,我们肯定会想,KOOM是如何追踪到OOM标识的,是如何异步进行dump的,接下来我们着重看下我们前面提到的各种检测器。

2.2.2 HeapOOMTracker

对于每一个检测器,我们只需要关注track方法即可

override fun track(): Boolean {
  /**第一步:获取进程内存占用率*/
  val heapRatio = SystemInfo.javaHeap.rate
  /**利用内存占用率 与 配置文件中的阈值做比较*/
  if (heapRatio > monitorConfig.heapThreshold
      && heapRatio >= mLastHeapRatio - HEAP_RATIO_THRESHOLD_GAP) {
    mOverThresholdCount++
    MonitorLog.i(TAG,
        "[meet condition] "
            + "overThresholdCount: $mOverThresholdCount"
            + ", heapRatio: $heapRatio"
            + ", usedMem: ${SizeUnit.BYTE.toMB(SystemInfo.javaHeap.used)}mb"
            + ", max: ${SizeUnit.BYTE.toMB(SystemInfo.javaHeap.max)}mb")
  } else {
    reset()
  }
  mLastHeapRatio = heapRatio
  return mOverThresholdCount >= monitorConfig.maxOverThresholdCount
}

首先第一步:获取当前进程内存占用率;我们看到代码中很简单的一行代码,但是真正要我们自己实现,可能就是个很大的麻烦,怎么计算内存占用率?

首先我们需要知道内存占用率需要哪两个值去计算?如果熟悉JVM虚拟机的伙伴应该了解有两个参数:-xmx和-xms,其中-xmx代表当前进程允许占用的最大内存(例如64M或者128M),-xms代表当前进程初始申请的内存,内存占用率就是这两个值的比例。

那么如何求出-xmx和-xms呢,我们看下快手团队是如何实现的。其实也是比较简单,因为就是调用系统API,但是很多伙伴可能比较陌生。

/**当前进程最大内存,-xmx*/
javaHeap.max = Runtime.getRuntime().maxMemory()
/**当前进程初始化申请的内存,-xms*/
javaHeap.total = Runtime.getRuntime().totalMemory()
/**当前进程剩余可用内存*/
javaHeap.free = Runtime.getRuntime().freeMemory()
javaHeap.used = javaHeap.total - javaHeap.free
javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max

注释已经添加,其中对于freeMemory我这里提一嘴,假设-xms为80M,freeMemory为30M,那么就说明当前进程已经占用了50M的内存,这也就是JavaHeap的used属性的结果。

private var mLastHeapRatio = 0.0f
private var mOverThresholdCount = 0
private const val HEAP_RATIO_THRESHOLD_GAP = 0.05f
if (heapRatio > monitorConfig.heapThreshold
    && heapRatio >= mLastHeapRatio - HEAP_RATIO_THRESHOLD_GAP)

当计算出内存占用率之后,我们看下面的一个判断条件,如果内存占用率超过我们设定的一个阈值(例如0.8),而且当前内存占用率跟上次比较超过了千分之5,那么mOverThresholdCount变量就会自增1。

因为检测是一个循环的过程,所以当第一次进来的时候,一定会自增1,而且会将本次的内存占用率赋值给mLastHeapRatio,当下次进来的时候,如果内存占用率较上次降低了,那么就会重置。

如此往复,当mOverThresholdCount超出我们设置的阈值(例如5次),我们就认定系统发生了内存泄漏,这个时候就需要告警,并dump内存快照分析问题。

2.2.3 ThreadOOMTracker

线程检测器跟内存检测器原理基本一致,同样也是在循环检测中,拿到线程的总数与阈值进行比较,如果超出范围那么就认为是异常,需要上报。

override fun track(): Boolean {
  val threadCount = getThreadCount()
  if (threadCount > monitorConfig.threadThreshold
      && threadCount >= mLastThreadCount - THREAD_COUNT_THRESHOLD_GAP) {
    mOverThresholdCount++
    MonitorLog.i(TAG,
        "[meet condition] "
            + "overThresholdCount:$mOverThresholdCount"
            + ", threadCount: $threadCount")
    dumpThreadIfNeed()
  } else {
    reset()
  }
  mLastThreadCount = threadCount
  return mOverThresholdCount >= monitorConfig.maxOverThresholdCount
}

这里获取系统线程总数,KOOM是通过读取配置文件的方式,如果在项目中有这个需求的伙伴,可以参考一下,注释已经加了。

File("/proc/self/status").forEachLineQuietly { line ->
  if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0
      && procStatus.thread != 0) return@forEachLineQuietly
  when {
    line.startsWith("VmSize") -> {
      procStatus.vssInKb = VSS_REGEX.matchValue(line)
    }
    line.startsWith("VmRSS") -> {
      procStatus.rssInKb = RSS_REGEX.matchValue(line)
    }
    /**获取线程数*/
    line.startsWith("Threads") -> {
      procStatus.thread = THREADS_REGEX.matchValue(line)
    }
  }
}

2.2.4 FastHugeMemoryOOMTracker

其他类型的检测器不再过多赘述,最后主要介绍一下FastHugeMemoryOOMTracker这个检测器,从名字看也是内存检测,但是跟HeapOOMTracker还是不一样的。

override fun track(): Boolean {
  val javaHeap = SystemInfo.javaHeap
  // 高危阈值直接触发dump分析
  if (javaHeap.rate > monitorConfig.forceDumpJavaHeapMaxThreshold) {
    mDumpReason = REASON_HIGH_WATERMARK
    MonitorLog.i(TAG, "[meet condition] fast huge memory allocated detected, " +
        "high memory watermark, force dump analysis!")
    return true
  }
  // 高差值直接dump
  val lastJavaHeap = SystemInfo.lastJavaHeap
  if (lastJavaHeap.max != 0L && javaHeap.used - lastJavaHeap.used
      > SizeUnit.KB.toByte(monitorConfig.forceDumpJavaHeapDeltaThreshold)) {
    mDumpReason = REASON_HUGE_DELTA
    MonitorLog.i(TAG, "[meet condition] fast huge memory allocated detected, " +
        "over the delta threshold!")
    return true
  }
  return false
}

从track方法中,我们可以看到,当进程内存占用率超过设定的forceDumpJavaHeapMaxThreshold阈值(例如0.9),直接返回了true。

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

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

2.3 dump为何不能放在子线程

前面我们着重介绍了各类内存检测工具的原理,其实他们的主要目的就是为了检测是否有OOM迹象的产生,这也是dump内存镜像的触发条件,如果只要有一个Tracker报警,紧接着往下就是要dump内存镜像。

首先我们在AS中使用Profile工具dump内存快照,其实就是基于JVMTI来实现的,前面在介绍Leakcanary的时候就已经说过,这个过程是非常耗时的,因为APM线上监控对于实时性的要求并不高,因此可以直接放在子线程或者子进程中完成。

private fun dumpAndAnalysis() {
  MonitorLog.i(TAG, "dumpAndAnalysis");
  runCatching {
    if (!OOMFileManager.isSpaceEnough()) {
      MonitorLog.e(TAG, "available space not enough", true)
      return@runCatching
    }
    if (mHasDumped) {
      return
    }
    mHasDumped = true
    val date = Date()
    val jsonFile = OOMFileManager.createJsonAnalysisFile(date)
    val hprofFile = OOMFileManager.createHprofAnalysisFile(date).apply {
      createNewFile()
      setWritable(true)
      setReadable(true)
    }
    MonitorLog.i(TAG, "hprof analysis dir:$hprofAnalysisDir")
    /**核心代码 在这里完成内存镜像的dump*/
    ForkJvmHeapDumper.getInstance().run {
      dump(hprofFile.absolutePath)
    }
    MonitorLog.i(TAG, "end hprof dump", true)
    Thread.sleep(1000) // make sure file synced to disk.
    MonitorLog.i(TAG, "start hprof analysis")
    startAnalysisService(hprofFile, jsonFile, mTrackReasons.joinToString())
  }.onFailure {
    it.printStackTrace()
    MonitorLog.i(TAG, "onJvmThreshold Exception " + it.message, true)
  }
}

在KOOM的dumpAndAnalysis方法中,我们看到创建了hprofFile文件,然后接下来一个核心类ForkJvmHeapDumper,这个类主要作用就是dump内存快照。

2.3.1 ForkJvmHeapDumper分析

看下这个类中的核心方法dump,传入的参数就是hprof文件的绝对路径

@Override
public synchronized boolean dump(String path) {
  MonitorLog.i(TAG, "dump " + path);
  if (!sdkVersionMatch()) {
    throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");
  }
  /**第一步,调用init方法,加载so文件*/
  init();
  if (!mLoadSuccess) {
    MonitorLog.e(TAG, "dump failed caused by so not loaded!");
    return false;
  }
  boolean dumpRes = false;
  try {
    MonitorLog.i(TAG, "before suspend and fork.");
    /**第二步,fork出一个子进程*/
    int pid = suspendAndFork();
    /**第三步,在子进程中完成dump*/
    if (pid == 0) {
      // Child process
      Debug.dumpHprofData(path);
      exitProcess();
    } else if (pid &gt; 0) {
      // Parent process
      dumpRes = resumeAndWait(pid);
      MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
    }
  } catch (IOException e) {
    MonitorLog.e(TAG, "dump failed caused by " + e);
    e.printStackTrace();
  }
  return dumpRes;
}

首先第一步,调用init方法,其主要目的就是加载一些相应的so文件,如果涉及到了so,那么肯定涉及到C++层代码的分析,虽然C++写的不好,但是还是能看懂一点点的

private void init () {
  if (mLoadSuccess) {
    return;
  }
  if (loadSoQuietly("koom-fast-dump")) {
    mLoadSuccess = true;
    nativeInit();
  }
}

然后第二步,调用suspendAndFork方法,这是一个native方法,看注释意思是挂起ART,然后创建一个进程去dump内存快照

/**
 * Suspend the whole ART, and then fork a process for dumping hprof.
 *
 * @return return value of fork
 */
private native int suspendAndFork();

首先如果从从到位跟到源码,应该记得在调用dumpAndAnalysis方法的时候,是在协程中也就是子线程中进行的。

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

子线程中不行吗?子线程也不会阻塞主线程,看起来似乎没问题,KOOM为啥要单独fork出一个单独的子进程去完成dump?

其实这样做的一个好处就是,虽然是在子线程内,但是还是会产生内存垃圾(一边采集数据,一边申请内存也不合理),还是需要GC去STW清理,如果放在单独的进程中,就不会加快主进程的GC,也是尽可能避免在dump时发生崩溃影响主进程。

除此之外,还有一个核心问题,是需要通过源码来一探究竟,dump的时候,系统底层到底做了什么?

2.3.2 C++层分析dumpHprofData

当子进程dump内存快照的时候,调用的是C++层的dumpHprofData函数,我们找下C++的源码看下。

public static void dumpHprofData(String fileName) throws IOException {
    VMDebug.dumpHprofData(fileName);
}

首先在Java层调用JNI层的代码就是VMDebug_dumpHprofData这个函数,最终是调用了Hprof的DumpHeap函数。

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);
}

在Hprof的DumpHeap函数中,创建了Hprof对象,并执行Dump方法,在此之前,我们可以看到是调用了ScopedSuspendAll。

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之前,是需要挂起一切的,看到这里,我们可能就知道了,不管是主线程还是子线程,只要进行了dump操作,都需要STW的。

2.4 多线程场景下fork进程

因为在任意线程中dump都会导致STW,所以KOOM是通过fork进程的方式完成dump操作的

MonitorLog.i(TAG, "before suspend and fork.");
int pid = suspendAndFork();
if (pid == 0) {
  // Child process
  Log.e("TAG","父进程fork成功,子进程开始执行")
  Debug.dumpHprofData(path);
  exitProcess();
  Log.e("TAG","子进程执行完成,退出")
} else if (pid &gt; 0) {
  Log.e("TAG","父进程fork成功,继续执行")
  // Parent process
  dumpRes = resumeAndWait(pid);
  MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}

首先调用suspendAndFork创建一个子进程,如果pid == 0,说明当前进程为子进程,那么会进入代码块执行,然后紧接着进入下一个代码块,最终的日志打印就是:

父进程fork成功,子进程开始执行
父进程fork成功,继续执行
子进程执行完成,退出

这是属于正常的fork流程,但是如果是在多线程的环境下呢?

val thread = Thread{
   Log.e("TAG","do something")
}
thread.start()
MonitorLog.i(TAG, "before suspend and fork.");
int pid = suspendAndFork();
if (pid == 0) {
  // Child process
  Log.e("TAG","父进程fork成功,子进程开始执行")
  Debug.dumpHprofData(path);
  exitProcess();
  Log.e("TAG","子进程执行完成,退出")
} else if (pid > 0) {
  Log.e("TAG","父进程fork成功,继续执行")
  // Parent process
  dumpRes = resumeAndWait(pid);
  MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}

这个时候,最终日志打印输出就是

父进程fork成功,子进程开始执行
父进程fork成功,继续执行

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

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

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

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

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

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

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

3 总结

所以回到开篇那个问题,如果需要我们自己设计一套线上APM监控,对于内存这块我们是不是就已经很清楚了,首先我们需要知道什么情况下会导致OOM,然后通过系统API来完成数据化监控方案;然后针对Leakcanary等成熟的框架存在的弊端,进行优化,例如子进程dump内存快照避免主线程卡顿等,当然在面试的过程中,如果有这方面的问题,是不是也得心应手了。

以上就是Android进阶KOOM线上APM监控全面剖析的详细内容,更多关于Android KOOM线上APM监控的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android隐私协议提示弹窗的实现流程详解

    android studio版本:2021.2.1 例程名称:pravicydialog 功能: 1.启动app后弹窗隐私协议 2.屏蔽返回键 3.再次启动不再显示隐私协议. 本例程的绝大部分代码来自下面链接,因为本人改了一些,增加了一些功能,所以不有脸的算原创了. 下面这个例子是“正宗”app隐私协议实现方法,而且协议内容使用的是txt格式文件,据说如果使用html格式文件,各大平台在审核的时候大概率无法通过,但协议内容的还应该有更详细协议及说明的链接,我没做,暂时还没学会,会了再修改一下.

  • Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解

    目录 引言 1 自定义滑动布局,实现吸顶效果 1.1 滑动容器实现 1.2 嵌套滑动机制完成交互优化 1.2.1 NestedScrollingParent接口和NestedScrollingChild接口 1.2.2 预滚动阶段实现 1.2.3 滚动阶段实现 1.2.4 滚动结束 引言 在上一篇文章Android进阶宝典 -- 事件冲突怎么解决?先从Android事件分发机制开始说起中,我们详细地介绍了Android事件分发机制,其实只要页面结构复杂,联动众多就会产生事件冲突,处理不得当就是b

  • Android进阶从字节码插桩技术了解美团热修复实例详解

    目录 引言 1 插件发布 2 Javassist 2.1 准备工作 2.2 Transform 2.3 transform函数注入代码 2.3.1 Jar包处理 2.3.2 字节码处理 2.4 Javassist织入代码 2.4.1 ClassPool 2.4.2 CtClass 引言 热修复技术如今已经不是一个新颖的技术,很多公司都在用,而且像阿里.腾讯等互联网巨头都有自己的热修复框架,像阿里的AndFix采用的是hook native底层修改代码指令集的方式:腾讯的Tinker采用类加载的方

  • Android进阶之从IO到NIO的模型机制演进

    目录 引言 1 Basic IO模型 1.1 RandomAccessFile的缓冲区和BufferedInputStream缓冲区的区别 1.2 Basic IO模型底层原理 2 NIO模型 3 OKIO 引言 其实IO操作相较于服务端,客户端做的并不多,基本的场景就是读写文件的时候会使用到InputStream或者OutputStream,然而客户端能做的就是发起一个读写的指令,真正的操作是内核层通过ioctl指令执行读写操作,因为每次的IO操作都涉及到了线程的操作,因此会有性能上的损耗,那

  • Android进阶Handler应用线上卡顿监控详解

    目录 引言 1 Handler消息机制 1.1 方案确认 1.2 Looper源码 1.3 Blockcanary原理分析 1.4 Handler监控的缺陷 2 字节码插桩实现方法耗时监控 2.1 字节码插桩流程 2.2 引入ASM实现字节码插桩 2.3 Blockcanary的优化策略 引言 在上一篇文章中# Android进阶宝典 -- KOOM线上APM监控最全剖析,我详细介绍了对于线上App内存监控的方案策略,其实除了内存指标之外,经常有用户反馈卡顿问题,其实这种问题是最难定位的,因为不

  • Android Jetpack组件ViewModel基本用法详解

    目录 引言 一.概述与作用 二.基本用法 小结 引言 天道好轮回,终于星期五,但是还是忙碌了一天.在项目中,我遇到了一个问题,起因则是无法实时去获取信息来更新UI界面,因为我需要知道我是否获取到了实时信息,我想到的办法有三,利用Handler收发消息在子线程与主线程切换从而更新信息,其二则是利用在页面重绘的时候(一般是页面变动如跳转下个页面和将应用切至后台),其三就是利用Jetpack中最重要的组件之一ViewModel,最后我还是选择了ViewModel,因为感觉更方便. 其实想到的前面两个方

  • Android进阶CoordinatorLayout协调者布局实现吸顶效果

    目录 引言 1 CoordinatorLayout功能介绍 1.1 CoordinatorLayout的依赖交互原理 1.2 CoordinatorLayout的嵌套滑动原理 2 CoordinatorLayout源码分析 2.1 CoordinatorLayout的依赖交互实现 2.2 CoordinatorLayout交互依赖的源码分析 2.3 CoordinatorLayout子控件拦截事件源码分析 2.4 CoordinatorLayout嵌套滑动原理分析 引言 在上一节Android进

  • Android进阶KOOM线上APM监控全面剖析

    目录 正文 1 Leakcanary为什么不能用于线上 1.1 Leakcanary原理简单剖析 1.2 小结 2 KOOM原理分析 2.1 KOOM引入 2.2 KOOM源码分析 2.2.1 trackOOM方法分析 2.2.2 HeapOOMTracker 2.2.3 ThreadOOMTracker 2.2.4 FastHugeMemoryOOMTracker 2.3 dump为何不能放在子线程 2.3.1 ForkJvmHeapDumper分析 2.3.2 C++层分析dumpHprof

  • 解析Arthas协助排查线上skywalking不可用问题

    目录 前言 使用到的工具arthas 先定位问题一 问题一: 问题解决: 功能说明 参数说明 定位问题二 问题二: 问题解决: 结语 前言 首先描述下问题的背景,博主有个习惯,每天上下班的时候看下skywalking的trace页面的error情况.但是某天突然发现生产环境skywalking页面没有任何数据了,页面也没有显示任何的异常,有点慌,我们线上虽然没有全面铺开对接skywalking,但是也有十多个应用.看了应用agent端日志后,其实也不用太担心,对应用毫无影响.大概情况就是这样,但

  • 线上MYSQL同步报错故障处理方法总结(必看篇)

    前言 在发生故障切换后,经常遇到的问题就是同步报错,数据库很小的时候,dump完再导入很简单就处理好了,但线上的数据库都150G-200G,如果用单纯的这种方法,成本太高,故经过一段时间的摸索,总结了几种处理方法. 生产环境架构图 目前现网的架构,保存着两份数据,通过异步复制做的高可用集群,两台机器提供对外服务.在发生故障时,切换到slave上,并将其变成master,坏掉的机器反向同步新的master,在处理故障时,遇到最多的就是主从报错.下面是我收录下来的报错信息. 常见错误 最常见的3种情

  • Android编程显示网络上的图片实例详解

    本文实例讲述了Android编程显示网络上的图片的方法.分享给大家供大家参考,具体如下: 在Android中显示网络上的图片,需要先根据url找到图片地址,然后把该图片转化成Java的InputStream,然后把该InputStream流转化成BitMap,BitMap可以直接显示在android中的ImageView里.这就是显示网络上图片的思路,实现起来很简单.下面让我们看一下实现起来的过程. 首先在AndroidManifest.xml中给程序加上访问Internet的权限: 复制代码

  • 详解Node.js项目APM监控之New Relic

    现在上一个项目,如果没有APM监控服务或应用的运行性能参数,等于是一架没有盲降系统的飞机正在盲降,结果会很悲催.出现了访问失效等问题时,都很难判定是性能瓶颈还是一个藏的深的bug,汇报的时候一顿眼晕,这样的结果肯定是要被人分分钟的各种撕. 目前还没有像样的给node.js项目应用的APM开源项目,暂且先羡慕嫉妒下给java用的PinPoint. 不用开源的就用专业的APM提供商的产品,先解决问题,国内有很多专业提供商,也是不错. 国外的New Relic也是被推荐的一家,集成进node.js特别

  • Android实现在map上画出路线的方法

    本文实例讲述了Android实现在map上画出路线的方法.分享给大家供大家参考.具体如下: 最近在搞在地图上画出路线图,经过一段时间的摸索,终于搞明白了,其实也挺简单的,写个类继承Overlay,并重写draw方法,在draw方法中画出 path即可.对于Overaly,在地图上标记某个点或者画线之类的就要使用overlay,overlay相当于一个覆盖物,覆盖在地图上,这个覆盖物要自己实现所以要继承Overlay. MapActivity.java如下: package net.blogjav

  • java开发 线上问题排查命令详解

    前言 作为一个合格的开发人员,不仅要能写得一手还代码,还有一项很重要的技能就是排查问题.这里提到的排查问题不仅仅是在coding的过程中debug等,还包括的就是线上问题的排查.由于在生产环境中,一般没办法debug(其实有些问题,debug也白扯...),所以我们需要借助一些常用命令来查看运行时的具体情况,这些运行时信息包括但不限于运行日志.异常堆栈.堆使用情况.GC情况.JVM参数情况.线程情况等. 给一个系统定位问题的时候,知识.经验是关键,数据是依据,工具是运用知识处理数据的手段.为了便

  • Android 进阶实现性能优化之OOM与Leakcanary详解原理

    目录 Android内存泄漏常见场景以及解决方案 资源性对象未关闭 注册对象未注销 类的静态变量持有大数据 单例造成的内存泄漏 非静态内部类的静态实例 Handler临时性内存泄漏 容器中的对象没清理造成的内存泄漏 WebView 使用ListView时造成的内存泄漏 Leakcanary leakcanary 导入 leakcanary 是如何安装的 leakcanary 如何监听Activity.Fragment销毁 RefWatcher 核心原理 流程图 本文主要探讨以下几个问题: And

  • Java线上问题排查神器Arthas实战原理解析

    概述 背景 是不是在实际开发工作当中经常碰到自己写的代码在开发.测试环境行云流水稳得一笔,可一到线上就经常不是缺这个就是少那个反正就是一顿报错抽风似的,线上调试代码又很麻烦,让人头疼得抓狂:而且debug不一定是最高效的方法,遇到线上问题不能debug了怎么办.原先我们Java中我们常用分析问题一般是使用JDK自带或第三方的分析工具如jstat.jmap.jstack. jconsole.visualvm.Java Mission Control.MAT等.但此刻的你没有看错,还有一款神器Art

随机推荐