Android進階KOOM線上APM監(jiān)控全面剖析
正文
APM,全稱是Application Performance Management,也就是應用性能管理,這與我們平時寫的業(yè)務可能并不相關,但是卻承載著App線上穩(wěn)定的責任。當一款App發(fā)布到線上之后,不同的用戶有不同場景,一旦App出現(xiàn)了問題,為了避免黑盒,找不到頭緒,就需要APM出馬了。
對于App的性能,像CPU、流量、電量、內存、crash、ANR,這些都會是監(jiān)控的點,尤其是當App發(fā)生崩潰的時候,需要回撈到當前用戶的日志加以分析,找到此問題崩潰的堆棧,完成修復。否則就像是大海撈針,根本不知道哪里發(fā)生了崩潰,查找問題可能就需要找一半天。
那么對于成熟的線上APM監(jiān)控,我們可能使用過Bugly、火山、Leakcanary,但其中都會有缺陷,對于一些大公司一般都會考慮自研APM,監(jiān)控的對象也無非上述這些指標,那么如果讓我們自己做一套APM監(jiān)控,該怎么出方案呢?
1 Leakcanary為什么不能用于線上
如果有做過APM監(jiān)控的伙伴,對于Leakcanary就很熟悉了,這個是一個老派的內存監(jiān)控組件,但是我們在使用的時候,通常都是采用debugImplementation的方式引入,在debug環(huán)境下使用,而不是線上,這是為什么呢?
這個還需要從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引用隊列,當一個對象被回收之后,就會被放在這個隊列中,從而與弱引用對象產生關聯(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: 弱引用隊列 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
}
}
這里有一個單例,在創(chuàng)建出一個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());
我們會發(fā)現(xiàn)無論如何GC,這個引用都無法被回收,因此對于內存泄漏的檢測,就可以使用弱引用配個引用隊列來進行關聯(lián)對象的檢測。
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中,就是采用這種方式進行內存泄漏的檢測,但是為啥不能用于線上,伙伴們應該知道,當系統(tǒng)在GC的時候,是需要STW的。
當一個Activity被銷毀之后,Leakcanary會在onDestory方法中進行2次GC(為啥要多次GC,其實是因為一次GC并不能保證對象被回收,可以通過上面的例子中看出),如果熟悉JVM的伙伴應該知道,只要涉及到GC,極大的概率會觸發(fā)STW,那么這個時候就會卡頓,如果有使用過Leakcanary,就會經常感受到卡頓甚至測試伙伴過來告訴你有bug,好在Leakcanary檢測到內存泄漏的時候會有一個全局動畫,不然真不好解釋了。
1.2 小結
對于Leakcanary不能應用于線上,從性能角度來說,前面我們已經介紹了,主要就是歸結于線程會STW;除此之外,因為Leakcanary在發(fā)生內存泄漏的時候,需要dump內存快照,生成hprof文件。


如果我們在Android Studio上分析過內存問題,會發(fā)現(xiàn)dump的過程非常耗時,會有3-4s的時間,有時甚至會卡死,但放在應用程序中,3-4s的時間可能直接導致ANR,因為整個過程應用程序是無響應的,所以Leakcanary只適合在線下測試環(huán)境中分析內存問題,不適合帶著上線。
2 KOOM原理分析
那么既然Leakcanary不能帶到線上,那么針對線上問題該如何分析呢?bugly只能分析Crash或者ANR,所以快手團隊針對這些問題,研發(fā)了KOOM線上內存監(jiān)控組件。
在此之前我們思考幾個問題:
(1)對于線上APM,它需要非常高的實時性嗎?如果出現(xiàn)內存泄漏就一定要立刻dump內存快照嗎?
(2)dump內存快照是否能夠在子線程中執(zhí)行,而不阻塞主線程;
(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源碼的伙伴應該明白,這肯定是循環(huán)的意思,當執(zhí)行startLoop方法的時候,就是開啟一個死循環(huán)。
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是要在主進程中開啟,然后執(zhí)行了父類方法的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來進行延遲消息的發(fā)送,執(zhí)行的就是這個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() && 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ù)組,我們看下這個數(shù)組是什么
private val mOOMTrackers = mutableListOf( HeapOOMTracker(), ThreadOOMTracker(), FdOOMTracker(), PhysicalMemoryOOMTracker(), FastHugeMemoryOOMTracker() )
這個數(shù)組其實是一些OOMTracker的實現(xiàn)類,就是這里大家需要思考一個問題,什么情況下會發(fā)生OOM?這里我總結一下主要可能發(fā)生OOM的場景:
(1)堆內存溢出;這個是典型的OOM場景;
(2)沒有連續(xù)的內存空間分配;這個主要是因為內存碎片過多(標記清除算法),導致即便內存夠用,也會造成OOM;
(3)打開過多的文件;如果有碰到這個異常OOM:open to many file的伙伴,應該就知道了;
(4)虛擬內存空間不足;
(5)開啟過多的線程;一般情況下,開啟一個線程大概會分配500k的內存,如果開啟線程過多同樣會導致OOM
所以看到這個數(shù)組中每個Tracker的名字,就應該明白,KOOM就是從這幾個方面入手,隨時監(jiān)控可能發(fā)生OOM的風險,并發(fā)出告警信息。
for (oomTracker in mOOMTrackers) {
if (oomTracker.track()) {
mTrackReasons.add(oomTracker.reason())
}
}
回到trackOOM這個方法,我們看在遍歷這個數(shù)組的過程中,每取出一個Tracker,都執(zhí)行了它的track方法
abstract class OOMTracker : Monitor<OOMMonitorConfig>() {
/**
* @return true 表示追蹤到oom、 false 表示沒有追蹤到oom
*/
abstract fun track(): Boolean
/**
* 重置track狀態(tài)
*/
abstract fun reset()
/**
* @return 追蹤到的oom的標識
*/
abstract fun reason(): String
}
我們看下SDK中的注釋,這個方法的帶有返回值的,如果返回了true,那么就表示追蹤到了OOM,如果返回了false,即代表沒有發(fā)生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
}
首先第一步:獲取當前進程內存占用率;我們看到代碼中很簡單的一行代碼,但是真正要我們自己實現(xiàn),可能就是個很大的麻煩,怎么計算內存占用率?
首先我們需要知道內存占用率需要哪兩個值去計算?如果熟悉JVM虛擬機的伙伴應該了解有兩個參數(shù):-xmx和-xms,其中-xmx代表當前進程允許占用的最大內存(例如64M或者128M),-xms代表當前進程初始申請的內存,內存占用率就是這兩個值的比例。
那么如何求出-xmx和-xms呢,我們看下快手團隊是如何實現(xiàn)的。其實也是比較簡單,因為就是調用系統(tǒng)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。
因為檢測是一個循環(huán)的過程,所以當?shù)谝淮芜M來的時候,一定會自增1,而且會將本次的內存占用率賦值給mLastHeapRatio,當下次進來的時候,如果內存占用率較上次降低了,那么就會重置。
如此往復,當mOverThresholdCount超出我們設置的閾值(例如5次),我們就認定系統(tǒng)發(fā)生了內存泄漏,這個時候就需要告警,并dump內存快照分析問題。
2.2.3 ThreadOOMTracker
線程檢測器跟內存檢測器原理基本一致,同樣也是在循環(huán)檢測中,拿到線程的總數(shù)與閾值進行比較,如果超出范圍那么就認為是異常,需要上報。
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是通過讀取配置文件的方式,如果在項目中有這個需求的伙伴,可以參考一下,注釋已經加了。
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
其他類型的檢測器不再過多贅述,最后主要介紹一下FastHugeMemoryOOMTracker這個檢測器,從名字看也是內存檢測,但是跟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方法中,我們可以看到,當進程內存占用率超過設定的forceDumpJavaHeapMaxThreshold閾值(例如0.9),直接返回了true。
這里是為啥呢?因為HeapOOMTracker屬于高內存持續(xù)監(jiān)測,需要連續(xù)多次檢測才會報警;但是如果我們程序中加載了一張大圖片,內存直接暴漲(超過0.9),可能都等不到HeapOOMTracker檢測多次程序直接Crash,這個時候就需要FastHugeMemoryOOMTracker出馬了,主要進入高危閾值,直接報警。
還有一個判斷條件就是,會比較前后兩次的內存使用情況,如果超出了閾值也會直接報警,例如加載大圖。
2.3 dump為何不能放在子線程
前面我們著重介紹了各類內存檢測工具的原理,其實他們的主要目的就是為了檢測是否有OOM跡象的產生,這也是dump內存鏡像的觸發(fā)條件,如果只要有一個Tracker報警,緊接著往下就是要dump內存鏡像。
首先我們在AS中使用Profile工具dump內存快照,其實就是基于JVMTI來實現(xiàn)的,前面在介紹Leakcanary的時候就已經說過,這個過程是非常耗時的,因為APM線上監(jiā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")
/**核心代碼 在這里完成內存鏡像的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文件,然后接下來一個核心類ForkJvmHeapDumper,這個類主要作用就是dump內存快照。
2.3.1 ForkJvmHeapDumper分析
看下這個類中的核心方法dump,傳入的參數(shù)就是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 > 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,然后創(chuàng)建一個進程去dump內存快照
/** * Suspend the whole ART, and then fork a process for dumping hprof. * * @return return value of fork */ private native int suspendAndFork();
首先如果從從到位跟到源碼,應該記得在調用dumpAndAnalysis方法的時候,是在協(xié)程中也就是子線程中進行的。
async {
MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")
dumpAndAnalysis()
}
子線程中不行嗎?子線程也不會阻塞主線程,看起來似乎沒問題,KOOM為啥要單獨fork出一個單獨的子進程去完成dump?
其實這樣做的一個好處就是,雖然是在子線程內,但是還是會產生內存垃圾(一邊采集數(shù)據(jù),一邊申請內存也不合理),還是需要GC去STW清理,如果放在單獨的進程中,就不會加快主進程的GC,也是盡可能避免在dump時發(fā)生崩潰影響主進程。
除此之外,還有一個核心問題,是需要通過源碼來一探究竟,dump的時候,系統(tǒng)底層到底做了什么?
2.3.2 C++層分析dumpHprofData
當子進程dump內存快照的時候,調用的是C++層的dumpHprofData函數(shù),我們找下C++的源碼看下。
public static void dumpHprofData(String fileName) throws IOException {
VMDebug.dumpHprofData(fileName);
}
首先在Java層調用JNI層的代碼就是VMDebug_dumpHprofData這個函數(shù),最終是調用了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;
/**調用Hprof的DumpHeap函數(shù)*/
hprof::DumpHeap(filename.c_str(), fd, false);
}
在Hprof的DumpHeap函數(shù)中,創(chuàng)建了Hprof對象,并執(zhí)行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成功,子進程開始執(zhí)行")
Debug.dumpHprofData(path);
exitProcess();
Log.e("TAG","子進程執(zhí)行完成,退出")
} else if (pid > 0) {
Log.e("TAG","父進程fork成功,繼續(xù)執(zhí)行")
// Parent process
dumpRes = resumeAndWait(pid);
MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}
首先調用suspendAndFork創(chuàng)建一個子進程,如果pid == 0,說明當前進程為子進程,那么會進入代碼塊執(zhí)行,然后緊接著進入下一個代碼塊,最終的日志打印就是:
父進程fork成功,子進程開始執(zhí)行
父進程fork成功,繼續(xù)執(zhí)行
子進程執(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","父進程fork成功,子進程開始執(zhí)行")
Debug.dumpHprofData(path);
exitProcess();
Log.e("TAG","子進程執(zhí)行完成,退出")
} else if (pid > 0) {
Log.e("TAG","父進程fork成功,繼續(xù)執(zhí)行")
// Parent process
dumpRes = resumeAndWait(pid);
MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
}
這個時候,最終日志打印輸出就是
父進程fork成功,子進程開始執(zhí)行
父進程fork成功,繼續(xù)執(zhí)行
子進程被卡死了,為什么呢?這就需要了解在fork進程時系統(tǒng)干了什么事!
當在父進程中fork子進程的時候,父進程的線程也會被拷貝到子進程當中,但是這個時候線程已經不是一個線程了,而是一個對象,任何線程的特性都不再存在,例如:
(1)父進程線程持有一個鎖對象,那么在子進程中這個鎖也會被復制過去,在子進程中如果想要競爭獲取這個鎖對象肯定是拿不到的,因為在對象頭中,這個是加鎖的,那么就會造成死鎖;
(2)因為在進程中進行dump的時候,是需要掛起線程的,因為此時線程都不再是一個線程,即便是調用掛起suspend也無效,無法獲取任何線程的返回值,子進程直接卡死。
那么KOOM是如何處理的呢,核心就在于suspendAndFork這個方法,在fork子進程之前先把所有的線程掛起,然后復制到子進程中的線程也是處于掛起的狀態(tài),就不會有卡死的這種情況發(fā)生;
然后在父進程中再次調用resumeAndWait方法,這個方法就會恢復線程的狀態(tài),雖然有一個短暫的掛起時間,但是相對于GC的頻繁STW,簡直不值一提了。
所以這里就有一個問題,我們知道在Android app啟動的時候,通過zygote來fork出主進程,這個時候AMS與zygote進程之間通信是通過socket而不是binder,這是為啥呢?原因就在這里了,看到這兒應該就懂了吧。
3 總結
所以回到開篇那個問題,如果需要我們自己設計一套線上APM監(jiān)控,對于內存這塊我們是不是就已經很清楚了,首先我們需要知道什么情況下會導致OOM,然后通過系統(tǒng)API來完成數(shù)據(jù)化監(jiān)控方案;然后針對Leakcanary等成熟的框架存在的弊端,進行優(yōu)化,例如子進程dump內存快照避免主線程卡頓等,當然在面試的過程中,如果有這方面的問題,是不是也得心應手了。
以上就是Android進階KOOM線上APM監(jiān)控全面剖析的詳細內容,更多關于Android KOOM線上APM監(jiān)控的資料請關注腳本之家其它相關文章!
相關文章
Android apk完整性檢測的實現(xiàn)思路和代碼實現(xiàn)
這篇文章主要介紹了Android apk完整性檢測的實現(xiàn)思路和代碼實現(xiàn),本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-12-12
Android中自定義View的實現(xiàn)方式總結大全
這篇文章主要總結了Android中自定義View的實現(xiàn)方式的相關資料,文中介紹的非常詳細,對各位Android開發(fā)者們學習或者使用自定義View具有一定的參考學習價值,需要的朋友們下面來一起看看吧。2017-04-04
詳解Android開發(fā)數(shù)據(jù)持久化之文件存儲(附源碼)
本篇文章主要介紹了詳解Android開發(fā)數(shù)據(jù)持久化之文件存儲(附源碼),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
如何判斷軟件程序是否聯(lián)網 聯(lián)網狀態(tài)提示信息Android實現(xiàn)
這篇文章主要為大家詳細介紹了如何判斷軟件程序是否聯(lián)網的實現(xiàn)代碼,Android實現(xiàn)聯(lián)網狀態(tài)信息提示,感興趣的小伙伴們可以參考一下2016-05-05

