欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android進(jìn)階KOOM線上APM監(jiān)控全面剖析

 更新時(shí)間:2023年01月29日 10:15:10   作者:layz4android  
這篇文章主要為大家介紹了Android進(jìn)階KOOM線上APM監(jiān)控全面剖析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

正文

APM,全稱是Application Performance Management,也就是應(yīng)用性能管理,這與我們平時(shí)寫的業(yè)務(wù)可能并不相關(guān),但是卻承載著App線上穩(wěn)定的責(zé)任。當(dāng)一款A(yù)pp發(fā)布到線上之后,不同的用戶有不同場(chǎng)景,一旦App出現(xiàn)了問題,為了避免黑盒,找不到頭緒,就需要APM出馬了。

對(duì)于App的性能,像CPU、流量、電量、內(nèi)存、crash、ANR,這些都會(huì)是監(jiān)控的點(diǎn),尤其是當(dāng)App發(fā)生崩潰的時(shí)候,需要回?fù)频疆?dāng)前用戶的日志加以分析,找到此問題崩潰的堆棧,完成修復(fù)。否則就像是大海撈針,根本不知道哪里發(fā)生了崩潰,查找問題可能就需要找一半天。

那么對(duì)于成熟的線上APM監(jiān)控,我們可能使用過(guò)Bugly、火山、Leakcanary,但其中都會(huì)有缺陷,對(duì)于一些大公司一般都會(huì)考慮自研APM,監(jiān)控的對(duì)象也無(wú)非上述這些指標(biāo),那么如果讓我們自己做一套APM監(jiān)控,該怎么出方案呢?

1 Leakcanary為什么不能用于線上

如果有做過(guò)APM監(jiān)控的伙伴,對(duì)于Leakcanary就很熟悉了,這個(gè)是一個(gè)老派的內(nèi)存監(jiān)控組件,但是我們?cè)谑褂玫臅r(shí)候,通常都是采用debugImplementation的方式引入,在debug環(huán)境下使用,而不是線上,這是為什么呢?

這個(gè)還需要從Leakcanary的原理說(shuō)起了。

1.1 Leakcanary原理簡(jiǎn)單剖析

對(duì)于Java的引用類型,大家應(yīng)該都清楚:強(qiáng)軟弱虛,接下來(lái)我們通過(guò)一個(gè)簡(jiǎn)單的示例,看下四種引用的特性,這里我主要是介紹一下弱引用

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","弱引用隊(duì)列 "+referenceQueue.poll());
System.gc();
Thread.sleep(2000);
Log.e("Test","弱引用 "+weak.get());
Log.e("Test","弱引用隊(duì)列 "+referenceQueue.poll());

在這里我們模擬了一次資源回收的GC操作,當(dāng)一個(gè)對(duì)象被置成null之后,通過(guò)gc正常情況下是可以被回收的;這里我們需要關(guān)注的是一個(gè)ReferenceQueue引用隊(duì)列,當(dāng)一個(gè)對(duì)象被回收之后,就會(huì)被放在這個(gè)隊(duì)列中,從而與弱引用對(duì)象產(chǎn)生關(guān)聯(lián)。

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: 弱引用隊(duì)列 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: 弱引用隊(duì)列 java.lang.ref.WeakReference@7cd1b13

那么這個(gè)時(shí)候我們模擬一下內(nèi)存泄漏

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

這里有一個(gè)單例,在創(chuàng)建出一個(gè)Object對(duì)象之后,就持有這個(gè)引用,然后這個(gè)時(shí)候把這個(gè)對(duì)象置為空

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","弱引用隊(duì)列 "+referenceQueue.poll());

我們會(huì)發(fā)現(xiàn)無(wú)論如何GC,這個(gè)引用都無(wú)法被回收,因此對(duì)于內(nèi)存泄漏的檢測(cè),就可以使用弱引用配個(gè)引用隊(duì)列來(lái)進(jìn)行關(guān)聯(lián)對(duì)象的檢測(cè)。

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: 弱引用隊(duì)列 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: 弱引用隊(duì)列 null

而在Leakcanary中,就是采用這種方式進(jìn)行內(nèi)存泄漏的檢測(cè),但是為啥不能用于線上,伙伴們應(yīng)該知道,當(dāng)系統(tǒng)在GC的時(shí)候,是需要STW的。

當(dāng)一個(gè)Activity被銷毀之后,Leakcanary會(huì)在onDestory方法中進(jìn)行2次GC(為啥要多次GC,其實(shí)是因?yàn)橐淮蜧C并不能保證對(duì)象被回收,可以通過(guò)上面的例子中看出),如果熟悉JVM的伙伴應(yīng)該知道,只要涉及到GC,極大的概率會(huì)觸發(fā)STW,那么這個(gè)時(shí)候就會(huì)卡頓,如果有使用過(guò)Leakcanary,就會(huì)經(jīng)常感受到卡頓甚至測(cè)試伙伴過(guò)來(lái)告訴你有bug,好在Leakcanary檢測(cè)到內(nèi)存泄漏的時(shí)候會(huì)有一個(gè)全局動(dòng)畫,不然真不好解釋了。

1.2 小結(jié)

對(duì)于Leakcanary不能應(yīng)用于線上,從性能角度來(lái)說(shuō),前面我們已經(jīng)介紹了,主要就是歸結(jié)于線程會(huì)STW;除此之外,因?yàn)長(zhǎng)eakcanary在發(fā)生內(nèi)存泄漏的時(shí)候,需要dump內(nèi)存快照,生成hprof文件。

如果我們?cè)贏ndroid Studio上分析過(guò)內(nèi)存問題,會(huì)發(fā)現(xiàn)dump的過(guò)程非常耗時(shí),會(huì)有3-4s的時(shí)間,有時(shí)甚至?xí)ㄋ?,但放在?yīng)用程序中,3-4s的時(shí)間可能直接導(dǎo)致ANR,因?yàn)檎麄€(gè)過(guò)程應(yīng)用程序是無(wú)響應(yīng)的,所以Leakcanary只適合在線下測(cè)試環(huán)境中分析內(nèi)存問題,不適合帶著上線。

2 KOOM原理分析

那么既然Leakcanary不能帶到線上,那么針對(duì)線上問題該如何分析呢?bugly只能分析Crash或者ANR,所以快手團(tuán)隊(duì)針對(duì)這些問題,研發(fā)了KOOM線上內(nèi)存監(jiān)控組件。

在此之前我們思考幾個(gè)問題:

(1)對(duì)于線上APM,它需要非常高的實(shí)時(shí)性嗎?如果出現(xiàn)內(nèi)存泄漏就一定要立刻dump內(nèi)存快照嗎?

(2)dump內(nèi)存快照是否能夠在子線程中執(zhí)行,而不阻塞主線程;

(3)對(duì)于生成的hprof文件,是否可以進(jìn)行裁剪,加快分析進(jìn)程盡快定位出問題來(lái)。

所以針對(duì)以上幾個(gè)問題,我們看下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}"

因?yàn)檎麄€(gè)KOOM的源碼都是Kotlin寫的,所以接下來(lái)的源碼分析都會(huì)是Kotlin為主,具體的使用如下,在初始化完成OOMMonitor,就調(diào)用startLoop方法開啟內(nèi)存檢測(cè)。

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方法,從這個(gè)方法名字中,我們大概就能猜到這個(gè)方法在干什么事,如果熟悉Handler源碼的伙伴應(yīng)該明白,這肯定是循環(huán)的意思,當(dāng)執(zhí)行startLoop方法的時(shí)候,就是開啟一個(gè)死循環(huán)。

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

首先startLoop是要在主進(jìn)程中開啟,然后執(zhí)行了父類方法的startLoop,那么我們跟進(jìn)去看一下。

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來(lái)進(jìn)行延遲消息的發(fā)送,執(zhí)行的就是這個(gè)mLoopRunnable。

private val mLoopRunnable = object : Runnable {
  override fun run() {
    /**進(jìn)行內(nèi)存泄漏、OOM檢測(cè)*/
    if (call() == LoopState.Terminate) {
      return
    }
    if (mIsLoopStopped) {
      return
    }
    getLoopHandler().removeCallbacks(this)
    getLoopHandler().postDelayed(this, getLoopInterval())
  }
}

在這個(gè)對(duì)象中,有一個(gè)核心方法call,就是用來(lái)做OOM和內(nèi)存泄漏的檢測(cè)

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

2.2.1 trackOOM方法分析

在call方法中,其實(shí)做的一個(gè)核心任務(wù)就是trackOOM,我們看下這個(gè)方法中主要是干了什么

private fun trackOOM(): LoopState {
  SystemInfo.refresh()
  mTrackReasons.clear()
  for (oomTracker in mOOMTrackers) {
    if (oomTracker.track()) {
      mTrackReasons.add(oomTracker.reason())
    }
  }
  /**如果追蹤到了OOM,那么就會(huì)異步分析*/
  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數(shù)組,我們看下這個(gè)數(shù)組是什么

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

這個(gè)數(shù)組其實(shí)是一些OOMTracker的實(shí)現(xiàn)類,就是這里大家需要思考一個(gè)問題,什么情況下會(huì)發(fā)生OOM?這里我總結(jié)一下主要可能發(fā)生OOM的場(chǎng)景:

(1)堆內(nèi)存溢出;這個(gè)是典型的OOM場(chǎng)景;

(2)沒有連續(xù)的內(nèi)存空間分配;這個(gè)主要是因?yàn)閮?nèi)存碎片過(guò)多(標(biāo)記清除算法),導(dǎo)致即便內(nèi)存夠用,也會(huì)造成OOM;

(3)打開過(guò)多的文件;如果有碰到這個(gè)異常OOM:open to many file的伙伴,應(yīng)該就知道了;

(4)虛擬內(nèi)存空間不足;

(5)開啟過(guò)多的線程;一般情況下,開啟一個(gè)線程大概會(huì)分配500k的內(nèi)存,如果開啟線程過(guò)多同樣會(huì)導(dǎo)致OOM

所以看到這個(gè)數(shù)組中每個(gè)Tracker的名字,就應(yīng)該明白,KOOM就是從這幾個(gè)方面入手,隨時(shí)監(jiān)控可能發(fā)生OOM的風(fēng)險(xiǎn),并發(fā)出告警信息。

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

回到trackOOM這個(gè)方法,我們看在遍歷這個(gè)數(shù)組的過(guò)程中,每取出一個(gè)Tracker,都執(zhí)行了它的track方法

abstract class OOMTracker : Monitor&lt;OOMMonitorConfig&gt;() {
  /**
   * @return true 表示追蹤到oom、 false 表示沒有追蹤到oom
   */
  abstract fun track(): Boolean
  /**
   * 重置track狀態(tài)
   */
  abstract fun reset()
  /**
   * @return 追蹤到的oom的標(biāo)識(shí)
   */
  abstract fun reason(): String
}

我們看下SDK中的注釋,這個(gè)方法的帶有返回值的,如果返回了true,那么就表示追蹤到了OOM,如果返回了false,即代表沒有發(fā)生OOM;

然后如果追蹤到了OOM,那么就將追蹤到OOM的標(biāo)識(shí)reason()塞到mTrackReasons這個(gè)集合當(dāng)中。后面就會(huì)判斷,如果這個(gè)集合不為空,那么就會(huì)去異步dump內(nèi)存快照并分析,而不去阻塞主線程。

所以看到這里,我們肯定會(huì)想,KOOM是如何追蹤到OOM標(biāo)識(shí)的,是如何異步進(jìn)行dump的,接下來(lái)我們著重看下我們前面提到的各種檢測(cè)器。

2.2.2 HeapOOMTracker

對(duì)于每一個(gè)檢測(cè)器,我們只需要關(guān)注track方法即可

override fun track(): Boolean {
  /**第一步:獲取進(jìn)程內(nèi)存占用率*/
  val heapRatio = SystemInfo.javaHeap.rate
  /**利用內(nèi)存占用率 與 配置文件中的閾值做比較*/
  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
}

首先第一步:獲取當(dāng)前進(jìn)程內(nèi)存占用率;我們看到代碼中很簡(jiǎn)單的一行代碼,但是真正要我們自己實(shí)現(xiàn),可能就是個(gè)很大的麻煩,怎么計(jì)算內(nèi)存占用率?

首先我們需要知道內(nèi)存占用率需要哪兩個(gè)值去計(jì)算?如果熟悉JVM虛擬機(jī)的伙伴應(yīng)該了解有兩個(gè)參數(shù):-xmx和-xms,其中-xmx代表當(dāng)前進(jìn)程允許占用的最大內(nèi)存(例如64M或者128M),-xms代表當(dāng)前進(jìn)程初始申請(qǐng)的內(nèi)存,內(nèi)存占用率就是這兩個(gè)值的比例。

那么如何求出-xmx和-xms呢,我們看下快手團(tuán)隊(duì)是如何實(shí)現(xiàn)的。其實(shí)也是比較簡(jiǎn)單,因?yàn)榫褪钦{(diào)用系統(tǒng)API,但是很多伙伴可能比較陌生。

/**當(dāng)前進(jìn)程最大內(nèi)存,-xmx*/
javaHeap.max = Runtime.getRuntime().maxMemory()
/**當(dāng)前進(jìn)程初始化申請(qǐng)的內(nèi)存,-xms*/
javaHeap.total = Runtime.getRuntime().totalMemory()
/**當(dāng)前進(jìn)程剩余可用內(nèi)存*/
javaHeap.free = Runtime.getRuntime().freeMemory()
javaHeap.used = javaHeap.total - javaHeap.free
javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max

注釋已經(jīng)添加,其中對(duì)于freeMemory我這里提一嘴,假設(shè)-xms為80M,freeMemory為30M,那么就說(shuō)明當(dāng)前進(jìn)程已經(jīng)占用了50M的內(nèi)存,這也就是JavaHeap的used屬性的結(jié)果。

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)

當(dāng)計(jì)算出內(nèi)存占用率之后,我們看下面的一個(gè)判斷條件,如果內(nèi)存占用率超過(guò)我們?cè)O(shè)定的一個(gè)閾值(例如0.8),而且當(dāng)前內(nèi)存占用率跟上次比較超過(guò)了千分之5,那么mOverThresholdCount變量就會(huì)自增1。

因?yàn)闄z測(cè)是一個(gè)循環(huán)的過(guò)程,所以當(dāng)?shù)谝淮芜M(jìn)來(lái)的時(shí)候,一定會(huì)自增1,而且會(huì)將本次的內(nèi)存占用率賦值給mLastHeapRatio,當(dāng)下次進(jìn)來(lái)的時(shí)候,如果內(nèi)存占用率較上次降低了,那么就會(huì)重置。

如此往復(fù),當(dāng)mOverThresholdCount超出我們?cè)O(shè)置的閾值(例如5次),我們就認(rèn)定系統(tǒng)發(fā)生了內(nèi)存泄漏,這個(gè)時(shí)候就需要告警,并dump內(nèi)存快照分析問題。

2.2.3 ThreadOOMTracker

線程檢測(cè)器跟內(nèi)存檢測(cè)器原理基本一致,同樣也是在循環(huán)檢測(cè)中,拿到線程的總數(shù)與閾值進(jìn)行比較,如果超出范圍那么就認(rèn)為是異常,需要上報(bào)。

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
}

這里獲取系統(tǒng)線程總數(shù),KOOM是通過(guò)讀取配置文件的方式,如果在項(xiàng)目中有這個(gè)需求的伙伴,可以參考一下,注釋已經(jīng)加了。

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)
    }
    /**獲取線程數(shù)*/
    line.startsWith("Threads") -> {
      procStatus.thread = THREADS_REGEX.matchValue(line)
    }
  }
}

2.2.4 FastHugeMemoryOOMTracker

其他類型的檢測(cè)器不再過(guò)多贅述,最后主要介紹一下FastHugeMemoryOOMTracker這個(gè)檢測(cè)器,從名字看也是內(nèi)存檢測(cè),但是跟HeapOOMTracker還是不一樣的。

override fun track(): Boolean {
  val javaHeap = SystemInfo.javaHeap
  // 高危閾值直接觸發(fā)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方法中,我們可以看到,當(dāng)進(jìn)程內(nèi)存占用率超過(guò)設(shè)定的forceDumpJavaHeapMaxThreshold閾值(例如0.9),直接返回了true。

這里是為啥呢?因?yàn)镠eapOOMTracker屬于高內(nèi)存持續(xù)監(jiān)測(cè),需要連續(xù)多次檢測(cè)才會(huì)報(bào)警;但是如果我們程序中加載了一張大圖片,內(nèi)存直接暴漲(超過(guò)0.9),可能都等不到HeapOOMTracker檢測(cè)多次程序直接Crash,這個(gè)時(shí)候就需要FastHugeMemoryOOMTracker出馬了,主要進(jìn)入高危閾值,直接報(bào)警。

還有一個(gè)判斷條件就是,會(huì)比較前后兩次的內(nèi)存使用情況,如果超出了閾值也會(huì)直接報(bào)警,例如加載大圖。

2.3 dump為何不能放在子線程

前面我們著重介紹了各類內(nèi)存檢測(cè)工具的原理,其實(shí)他們的主要目的就是為了檢測(cè)是否有OOM跡象的產(chǎn)生,這也是dump內(nèi)存鏡像的觸發(fā)條件,如果只要有一個(gè)Tracker報(bào)警,緊接著往下就是要dump內(nèi)存鏡像。

首先我們?cè)贏S中使用Profile工具dump內(nèi)存快照,其實(shí)就是基于JVMTI來(lái)實(shí)現(xiàn)的,前面在介紹Leakcanary的時(shí)候就已經(jīng)說(shuō)過(guò),這個(gè)過(guò)程是非常耗時(shí)的,因?yàn)锳PM線上監(jiān)控對(duì)于實(shí)時(shí)性的要求并不高,因此可以直接放在子線程或者子進(jìn)程中完成。

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")
    /**核心代碼 在這里完成內(nèi)存鏡像的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方法中,我們看到創(chuàng)建了hprofFile文件,然后接下來(lái)一個(gè)核心類ForkJvmHeapDumper,這個(gè)類主要作用就是dump內(nèi)存快照。

2.3.1 ForkJvmHeapDumper分析

看下這個(gè)類中的核心方法dump,傳入的參數(shù)就是hprof文件的絕對(duì)路徑

@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!");
  }
  /**第一步,調(diào)用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出一個(gè)子進(jìn)程*/
    int pid = suspendAndFork();
    /**第三步,在子進(jìn)程中完成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;
}

首先第一步,調(diào)用init方法,其主要目的就是加載一些相應(yīng)的so文件,如果涉及到了so,那么肯定涉及到C++層代碼的分析,雖然C++寫的不好,但是還是能看懂一點(diǎn)點(diǎn)的

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

然后第二步,調(diào)用suspendAndFork方法,這是一個(gè)native方法,看注釋意思是掛起ART,然后創(chuàng)建一個(gè)進(jìn)程去dump內(nèi)存快照

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

首先如果從從到位跟到源碼,應(yīng)該記得在調(diào)用dumpAndAnalysis方法的時(shí)候,是在協(xié)程中也就是子線程中進(jìn)行的。

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

子線程中不行嗎?子線程也不會(huì)阻塞主線程,看起來(lái)似乎沒問題,KOOM為啥要單獨(dú)fork出一個(gè)單獨(dú)的子進(jìn)程去完成dump?

其實(shí)這樣做的一個(gè)好處就是,雖然是在子線程內(nèi),但是還是會(huì)產(chǎn)生內(nèi)存垃圾(一邊采集數(shù)據(jù),一邊申請(qǐng)內(nèi)存也不合理),還是需要GC去STW清理,如果放在單獨(dú)的進(jìn)程中,就不會(huì)加快主進(jìn)程的GC,也是盡可能避免在dump時(shí)發(fā)生崩潰影響主進(jìn)程。

除此之外,還有一個(gè)核心問題,是需要通過(guò)源碼來(lái)一探究竟,dump的時(shí)候,系統(tǒng)底層到底做了什么?

2.3.2 C++層分析dumpHprofData

當(dāng)子進(jìn)程dump內(nèi)存快照的時(shí)候,調(diào)用的是C++層的dumpHprofData函數(shù),我們找下C++的源碼看下。

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

首先在Java層調(diào)用JNI層的代碼就是VMDebug_dumpHprofData這個(gè)函數(shù),最終是調(diào)用了Hprof的DumpHeap函數(shù)。

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;
  /**調(diào)用Hprof的DumpHeap函數(shù)*/
  hprof::DumpHeap(filename.c_str(), fd, false);
}

在Hprof的DumpHeap函數(shù)中,創(chuàng)建了Hprof對(duì)象,并執(zhí)行Dump方法,在此之前,我們可以看到是調(diào)用了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();
    }

也就是說(shuō),在dump之前,是需要掛起一切的,看到這里,我們可能就知道了,不管是主線程還是子線程,只要進(jìn)行了dump操作,都需要STW的。

2.4 多線程場(chǎng)景下fork進(jìn)程

因?yàn)樵谌我饩€程中dump都會(huì)導(dǎo)致STW,所以KOOM是通過(guò)fork進(jìn)程的方式完成dump操作的

MonitorLog.i(TAG, "before suspend and fork.");
int pid = suspendAndFork();
if (pid == 0) {
  // Child process
  Log.e("TAG","父進(jìn)程fork成功,子進(jìn)程開始執(zhí)行")
  Debug.dumpHprofData(path);
  exitProcess();
  Log.e("TAG","子進(jìn)程執(zhí)行完成,退出")
} else if (pid &gt; 0) {
  Log.e("TAG","父進(jìn)程fork成功,繼續(xù)執(zhí)行")
  // Parent process
  dumpRes = resumeAndWait(pid);
  MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}

首先調(diào)用suspendAndFork創(chuàng)建一個(gè)子進(jìn)程,如果pid == 0,說(shuō)明當(dāng)前進(jìn)程為子進(jìn)程,那么會(huì)進(jìn)入代碼塊執(zhí)行,然后緊接著進(jìn)入下一個(gè)代碼塊,最終的日志打印就是:

父進(jìn)程fork成功,子進(jìn)程開始執(zhí)行
父進(jìn)程fork成功,繼續(xù)執(zhí)行
子進(jìn)程執(zhí)行完成,退出

這是屬于正常的fork流程,但是如果是在多線程的環(huán)境下呢?

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","父進(jìn)程fork成功,子進(jìn)程開始執(zhí)行")
  Debug.dumpHprofData(path);
  exitProcess();
  Log.e("TAG","子進(jìn)程執(zhí)行完成,退出")
} else if (pid > 0) {
  Log.e("TAG","父進(jìn)程fork成功,繼續(xù)執(zhí)行")
  // Parent process
  dumpRes = resumeAndWait(pid);
  MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}

這個(gè)時(shí)候,最終日志打印輸出就是

父進(jìn)程fork成功,子進(jìn)程開始執(zhí)行
父進(jìn)程fork成功,繼續(xù)執(zhí)行

子進(jìn)程被卡死了,為什么呢?這就需要了解在fork進(jìn)程時(shí)系統(tǒng)干了什么事!

當(dāng)在父進(jìn)程中fork子進(jìn)程的時(shí)候,父進(jìn)程的線程也會(huì)被拷貝到子進(jìn)程當(dāng)中,但是這個(gè)時(shí)候線程已經(jīng)不是一個(gè)線程了,而是一個(gè)對(duì)象,任何線程的特性都不再存在,例如:

(1)父進(jìn)程線程持有一個(gè)鎖對(duì)象,那么在子進(jìn)程中這個(gè)鎖也會(huì)被復(fù)制過(guò)去,在子進(jìn)程中如果想要競(jìng)爭(zhēng)獲取這個(gè)鎖對(duì)象肯定是拿不到的,因?yàn)樵趯?duì)象頭中,這個(gè)是加鎖的,那么就會(huì)造成死鎖;

(2)因?yàn)樵谶M(jìn)程中進(jìn)行dump的時(shí)候,是需要掛起線程的,因?yàn)榇藭r(shí)線程都不再是一個(gè)線程,即便是調(diào)用掛起suspend也無(wú)效,無(wú)法獲取任何線程的返回值,子進(jìn)程直接卡死。

那么KOOM是如何處理的呢,核心就在于suspendAndFork這個(gè)方法,在fork子進(jìn)程之前先把所有的線程掛起,然后復(fù)制到子進(jìn)程中的線程也是處于掛起的狀態(tài),就不會(huì)有卡死的這種情況發(fā)生;

然后在父進(jìn)程中再次調(diào)用resumeAndWait方法,這個(gè)方法就會(huì)恢復(fù)線程的狀態(tài),雖然有一個(gè)短暫的掛起時(shí)間,但是相對(duì)于GC的頻繁STW,簡(jiǎn)直不值一提了。

所以這里就有一個(gè)問題,我們知道在Android app啟動(dòng)的時(shí)候,通過(guò)zygote來(lái)fork出主進(jìn)程,這個(gè)時(shí)候AMS與zygote進(jìn)程之間通信是通過(guò)socket而不是binder,這是為啥呢?原因就在這里了,看到這兒應(yīng)該就懂了吧。

3 總結(jié)

所以回到開篇那個(gè)問題,如果需要我們自己設(shè)計(jì)一套線上APM監(jiān)控,對(duì)于內(nèi)存這塊我們是不是就已經(jīng)很清楚了,首先我們需要知道什么情況下會(huì)導(dǎo)致OOM,然后通過(guò)系統(tǒng)API來(lái)完成數(shù)據(jù)化監(jiān)控方案;然后針對(duì)Leakcanary等成熟的框架存在的弊端,進(jìn)行優(yōu)化,例如子進(jìn)程dump內(nèi)存快照避免主線程卡頓等,當(dāng)然在面試的過(guò)程中,如果有這方面的問題,是不是也得心應(yīng)手了。

以上就是Android進(jìn)階KOOM線上APM監(jiān)控全面剖析的詳細(xì)內(nèi)容,更多關(guān)于Android KOOM線上APM監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 在啟動(dòng)欄制作android studio啟動(dòng)圖標(biāo)

    在啟動(dòng)欄制作android studio啟動(dòng)圖標(biāo)

    這篇文章主要介紹了在啟動(dòng)欄制作android studio啟動(dòng)圖標(biāo)的相關(guān)知識(shí),需要的朋友可以參考下
    2018-03-03
  • Android自定義實(shí)現(xiàn)日歷控件

    Android自定義實(shí)現(xiàn)日歷控件

    這篇文章主要為大家詳細(xì)介紹了Android自定義實(shí)現(xiàn)日歷控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • Flutter Map常用操作方法總結(jié)

    Flutter Map常用操作方法總結(jié)

    Flutter 中的 Map 是一種鍵值對(duì)的集合,可以存儲(chǔ)任意類型的數(shù)據(jù),并且可以通過(guò)鍵來(lái)訪問和操作對(duì)應(yīng)的值,下面我們就來(lái)學(xué)習(xí)一下Flutter Map的常用操作方法吧
    2023-11-11
  • Android webview與js的數(shù)據(jù)交互

    Android webview與js的數(shù)據(jù)交互

    有了WebView這個(gè)組件,Android應(yīng)用開發(fā)技術(shù)也就轉(zhuǎn)嫁到html與java數(shù)據(jù)交互上來(lái)。說(shuō)白了就是js與WebView的數(shù)據(jù)交互,這就是本文所要討論的
    2017-04-04
  • Android菜單的定義及ActionBar的實(shí)現(xiàn)

    Android菜單的定義及ActionBar的實(shí)現(xiàn)

    本篇文章主要介紹了Android菜單的定義及ActionBar的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-08-08
  • Android自定義拋出異常的方法詳解

    Android自定義拋出異常的方法詳解

    這篇文章主要給大家介紹了關(guān)于Android自定義拋出異常的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-06-06
  • Android setTag方法的key問題解決辦法

    Android setTag方法的key問題解決辦法

    這篇文章主要介紹了Android setTag方法的key問題解決辦法的相關(guān)資料,需要的朋友可以參考下
    2016-09-09
  • AndroidStudio接入U(xiǎn)nity工程并實(shí)現(xiàn)相互跳轉(zhuǎn)的示例代碼

    AndroidStudio接入U(xiǎn)nity工程并實(shí)現(xiàn)相互跳轉(zhuǎn)的示例代碼

    這篇文章主要介紹了AndroidStudio接入U(xiǎn)nity工程并實(shí)現(xiàn)相互跳轉(zhuǎn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-12-12
  • flutter布局約束原理深入解析

    flutter布局約束原理深入解析

    這篇文章主要為大家介紹了flutter布局約束原理深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • android APP登陸頁(yè)面適配的實(shí)現(xiàn)

    android APP登陸頁(yè)面適配的實(shí)現(xiàn)

    這篇文章主要介紹了android APP登陸頁(yè)面適配的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-09-09

最新評(píng)論