【Android 内存优化】KOOM 快手开源框架线上内存监控方案-源码剖析

文章目录

  • 前言
  • OOMMonitorInitTask.INSTANCE.init
  • OOMMonitor.INSTANCE.startLoop
  • super.startLoop
    • call() == LoopState.Terminate
  • dumpAndAnalysis
  • dump
  • startAnalysisService
  • 回到startLoop方法
  • 总结

前言

这篇文章主要剖析KOOM的Java层源码设计逻辑。

使用篇请看上一篇:
【Android KOOM】KOOM java leak使用全解析

OOMMonitorInitTask.INSTANCE.init

OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());

这里进行初始化,来看看init里面做了什么:

object OOMMonitorInitTask : InitTask {override fun init(application: Application) {val config = OOMMonitorConfig.Builder().setThreadThreshold(50) //50 only for test! Please use default value!.setFdThreshold(300) // 300 only for test! Please use default value!.setHeapThreshold(0.9f) // 0.9f for test! Please use default value!.setVssSizeThreshold(1_000_000) // 1_000_000 for test! Please use default value!.setMaxOverThresholdCount(1) // 1 for test! Please use default value!.setAnalysisMaxTimesPerVersion(3) // Consider use default value!.setAnalysisPeriodPerVersion(15 * 24 * 60 * 60 * 1000) // Consider use default value!.setLoopInterval(5_000) // 5_000 for test! Please use default value!.setEnableHprofDumpAnalysis(true).setHprofUploader(object : OOMHprofUploader {override fun upload(file: File, type: OOMHprofUploader.HprofType) {MonitorLog.e("OOMMonitor", "todo, upload hprof ${file.name} if necessary")}}).setReportUploader(object : OOMReportUploader {override fun upload(file: File, content: String) {MonitorLog.i("OOMMonitor", content)MonitorLog.e("OOMMonitor", "todo, upload report ${file.name} if necessary")}}).build()MonitorManager.addMonitorConfig(config)}
}

可以看到里面做了各种参数的配置,包括上传hprof和报告的上传回调。
使用了构建者模式来进行参数设置,接着通过MonitorManager.addMonitorConfig(config) 添加到MonitorManager中,可见MonitorManager这个类就是监控器管理用的。

interface InitTask {fun init(application: Application)
}

定义了一个接口,用来初始化内存监控任务。参数是需要传递Application,但是这里没有看到有使用到。

OOMMonitor.INSTANCE.startLoop

        OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

上面配置好咯参数和回调,这里就是开始循环。下面来看看里面做了什么。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {
@Volatileprivate var mIsLoopStarted = false
...override fun startLoop(clearQueue: Boolean, postAtFront: Boolean, delayMillis: Long) {throwIfNotInitialized { return }if (!isMainProcess()) {return}MonitorLog.i(TAG, "startLoop()")if (mIsLoopStarted) {return}mIsLoopStarted = truesuper.startLoop(clearQueue, postAtFront, delayMillis)getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)}...}

判断下,假如非主线程,立刻返回。这里可以看出来,调用的地方必须是主线程,不然它就不会执行。
来看下mIsLoopStarted,它被Volatile修饰。Volatile的作用是可以把对应的变量刷新到Cpu缓存中,保证了多线程环境变量的可见性。假如有其他线程修改了这个变量,那么其他线程可以立刻知道。
而这里判断假如loop已经开始,那么也return掉。这些属于健壮性代码。

super.startLoop

看下super.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}

这里可看到围绕着mLoopRunnable来做功夫。首先看看是否需要清理之前的mLoopRunnable,接着根据参数,决定把runable post到消息队列的哪种情况中,这个稍后研究。这里先看看哪里传入的Handler。

通过跳转,找到了这里:

package com.kwai.koom.base.loopimport android.os.Handler
import android.os.HandlerThread
import android.os.Process.THREAD_PRIORITY_BACKGROUNDinternal object LoopThread : HandlerThread("LoopThread", THREAD_PRIORITY_BACKGROUND) {init {start()}internal val LOOP_HANDLER = Handler(LoopThread.looper)
}

这里是一个HandlerThread,至于HandlerThread。并且LoopThread它在初始化就执行start方法来启动线程。

接着看mLoopRunnable

protected open fun getLoopInterval(): Long {return DEFAULT_LOOP_INTERVAL}companion object {private const val DEFAULT_LOOP_INTERVAL = 1000L}private val mLoopRunnable = object : Runnable {override fun run() {if (call() == LoopState.Terminate) {return}if (mIsLoopStopped) {return}getLoopHandler().removeCallbacks(this)getLoopHandler().postDelayed(this, getLoopInterval())}}

这里就是拿到handler,执行postDelayed,间隔设置为1秒。

call() == LoopState.Terminate

这行代码是关键,假如LoopState.Terminate,是结束状态的话,那就执行call方法。
在这里插入图片描述
看下OOMMonitor的实现:

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

假如dump完成,就返回terminate状态。继续看trackOOM方法:

 private fun trackOOM(): LoopState {SystemInfo.refresh()mTrackReasons.clear()for (oomTracker in mOOMTrackers) {if (oomTracker.track()) {mTrackReasons.add(oomTracker.reason())}}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}

看下refresh方法:

var procStatus = ProcStatus()var lastProcStatus = ProcStatus()var memInfo = MemInfo()var lastMemInfo = MemInfo()var javaHeap = JavaHeap()var lastJavaHeap = JavaHeap()fun refresh() {lastJavaHeap = javaHeaplastMemInfo = memInfolastProcStatus = procStatusjavaHeap = JavaHeap()procStatus = ProcStatus()memInfo = MemInfo()javaHeap.max = Runtime.getRuntime().maxMemory()javaHeap.total = Runtime.getRuntime().totalMemory()javaHeap.free = Runtime.getRuntime().freeMemory()javaHeap.used = javaHeap.total - javaHeap.freejavaHeap.rate = 1.0f * javaHeap.used / javaHeap.maxFile("/proc/self/status").forEachLineQuietly { line ->if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0&& procStatus.thread != 0) return@forEachLineQuietlywhen {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)}}}File("/proc/meminfo").forEachLineQuietly { line ->when {line.startsWith("MemTotal") -> {memInfo.totalInKb = MEM_TOTAL_REGEX.matchValue(line)}line.startsWith("MemFree") -> {memInfo.freeInKb = MEM_FREE_REGEX.matchValue(line)}line.startsWith("MemAvailable") -> {memInfo.availableInKb = MEM_AVA_REGEX.matchValue(line)}line.startsWith("CmaTotal") -> {memInfo.cmaTotal = MEM_CMA_REGEX.matchValue(line)}line.startsWith("ION_heap") -> {memInfo.IONHeap = MEM_ION_REGEX.matchValue(line)}}}memInfo.rate = 1.0f * memInfo.availableInKb / memInfo.totalInKbMonitorLog.i(TAG, "----OOM Monitor Memory----")MonitorLog.i(TAG,"[java] max:${javaHeap.max} used ratio:${(javaHeap.rate * 100).toInt()}%")MonitorLog.i(TAG,"[proc] VmSize:${procStatus.vssInKb}kB VmRss:${procStatus.rssInKb}kB " + "Threads:${procStatus.thread}")MonitorLog.i(TAG,"[meminfo] MemTotal:${memInfo.totalInKb}kB MemFree:${memInfo.freeInKb}kB " + "MemAvailable:${memInfo.availableInKb}kB")MonitorLog.i(TAG,"avaliable ratio:${(memInfo.rate * 100).toInt()}% CmaTotal:${memInfo.cmaTotal}kB ION_heap:${memInfo.IONHeap}kB")}

SystemInfo类里面有很多Java堆,内存信息,进程状态相关的类。这里面可以看出,这个类就是用来把一些监控到的数据刷新和写入文件里面的。当然,还有log输出。

再看mOOMTrackers,分别是各个跟踪器

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

他们抽象父类是:

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

至于具体怎么track,由于篇幅和内容方向问题,这篇文章先不进一步分析。留到后面的文章继续。

回到trackOOM方法:

   mTrackReasons.clear()for (oomTracker in mOOMTrackers) {if (oomTracker.track()) {mTrackReasons.add(oomTracker.reason())}}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()}}

假如track到了原因,它就添加mTrackReasons。
假如分析超过时间和次数,就打印error。其它正常情况就打印mTrackReasons,执行dumpAndAnalysis,然后返回LoopState.Terminate状态。

下面重点看看dumpAndAnalysis方法:

dumpAndAnalysis

 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 = trueval 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")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)}}

这里面正式把track到的数据写入到文件中,包括json文件和hprof文件。重点看dump方法:

dump

@Overridepublic synchronized boolean dump(String path) {MonitorLog.i(TAG, "dump " + path);if (!sdkVersionMatch()) {throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");}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.");int pid = suspendAndFork();if (pid == 0) {// Child processDebug.dumpHprofData(path);exitProcess();} else if (pid > 0) {// Parent processdumpRes = 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方法:

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

这里加载一个so库,可以看到还有这些native方法:

/*** Init before do dump.*/private native void nativeInit();/*** Suspend the whole ART, and then fork a process for dumping hprof.** @return return value of fork*/private native int suspendAndFork();/*** Resume the whole ART, and then wait child process to notify.** @param pid pid of child process.*/private native boolean resumeAndWait(int pid);/*** Exit current process.*/private native void exitProcess();

接着执行suspendAndFork,也是native方法。拿到进程pid之后,fork当前进程。然后dump hprof文件。
在这里插入图片描述
至于为什么需要fork一个进程出来dump,可以通过上面截图看出来原因,dump hprof 数据的时候会触发GC,而GC会出发STW,这无疑会造成APP卡顿。这也是LeakCanary不能做成线上内存监控的主要原因,而KOOM解决了这个问题。

子进程dump工作做完之后,接着exitProcess退出。

假如pid > 0,resumeAndWait,就恢复整个ART虚拟机,然后等待子线程唤醒。

这里逻辑我说的有点不清晰,由于看不到so的代码,无法确认。有知道的大佬可以指点一下,感激。

startAnalysisService

前面fork子进程后,执行了 Thread.sleep(1000) // make sure file synced to disk.
接着看是分析堆转信息工作:

private fun startAnalysisService(hprofFile: File,jsonFile: File,reason: String) {if (hprofFile.length() == 0L) {hprofFile.delete()MonitorLog.i(TAG, "hprof file size 0", true)return}if (!getApplication().isForeground) {MonitorLog.e(TAG, "try startAnalysisService, but not foreground")mForegroundPendingRunnables.add(Runnable {startAnalysisService(hprofFile,jsonFile,reason)})return}OOMPreferenceManager.increaseAnalysisTimes()val extraData = AnalysisExtraData().apply {this.reason = reasonthis.currentPage = getApplication().currentActivity?.localClassName.orEmpty()this.usageSeconds = "${(SystemClock.elapsedRealtime() - mMonitorInitTime) / 1000}"}HeapAnalysisService.startAnalysisService(getApplication(),hprofFile.canonicalPath,jsonFile.canonicalPath,extraData,object : AnalysisReceiver.ResultCallBack {override fun onError() {MonitorLog.e(TAG, "heap analysis error, do file delete", true)hprofFile.delete()jsonFile.delete()}override fun onSuccess() {MonitorLog.i(TAG, "heap analysis success, do upload", true)val content = jsonFile.readText()MonitorLogger.addExceptionEvent(content, Logger.ExceptionType.OOM_STACKS)monitorConfig.reportUploader?.upload(jsonFile, content)monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.ORIGIN)}})}

这里就是进行针对一些dump数据进行解析、整理等工作,假如需要上传到服务器,这里也预留了接口供开发者使用,非常贴心。

到这里KOOM框架的Java层核心代码逻辑基本过完了。

回到startLoop方法

回到startLoop方法中super.startLoop 方法,下一行代码是:

    getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)

前面分析知道,getLoopHandler拿到的是HandlerThread,这里延时post一个runable消息给它。这里使用协程来执行。

重点需要关注的是processOldHprofFile。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {private const val TAG = "OOMMonitor"...private fun processOldHprofFile() {MonitorLog.i(TAG, "processHprofFile")if (mHasProcessOldHprof) {return}mHasProcessOldHprof = true;reAnalysisHprof()manualDumpHprof()}...private fun reAnalysisHprof() {for (file in hprofAnalysisDir.listFiles().orEmpty()) {if (!file.exists()) continueif (!file.name.startsWith(MonitorBuildConfig.VERSION_NAME)) {MonitorLog.i(TAG, "delete other version files ${file.name}")file.delete()continue}if (file.canonicalPath.endsWith(".hprof")) {val jsonFile = File(file.canonicalPath.replace(".hprof", ".json"))if (!jsonFile.exists()) {MonitorLog.i(TAG, "create json file and then start service")jsonFile.createNewFile()startAnalysisService(file, jsonFile, "reanalysis")} else {MonitorLog.i(TAG,if (jsonFile.length() == 0L) "last analysis isn't succeed, delete file"else "delete old files", true)jsonFile.delete()file.delete()}}}}private fun manualDumpHprof() {for (hprofFile in manualDumpDir.listFiles().orEmpty()) {MonitorLog.i(TAG, "manualDumpHprof upload:${hprofFile.absolutePath}")monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.STRIPPED)}}}

里面就是操作dump出来的文件,判断当前的版本,假如是旧的,删掉重写等逻辑。

总结

截止到这里,我们开始监控的这两行代码分析完毕:

  /** Init OOMMonitor*/OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

很简单的两行代码,里面包含了如此之多的业务逻辑和精彩的设计。

很多时候,我们使用越是简单的开源框架,越是能证明作者的厉害之处。他们把繁杂的逻辑内聚到了框架里面,让使用者能用简单一两行代码实现复杂的逻辑业务。

KOOM作为一个线上内存监控框架,有很多优秀的设计。这篇文章也只是在外层分析了一些表面的技术逻辑,至于更深入的内容,后续会继续更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/526199.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实验二(一):IPV4编址及IPV4路由基础实验

一实验介绍 1.关于本实验 IPv4( Internet Protocol Version 4)是 TCP/IP 协议族中最为核心的协议之一。 它工作在 TCP/IP参考模型的网际互联层&#xff0c;该层与 OSI参考模型的网络层相对应。 网络层提供了无连接数据传输服务&#xff0c;即网络在发送分组时不需要先建立连…

深度学习在硬件和计算平台上的优化:实现更快、更高效的突破

引言 深度学习&#xff0c;作为机器学习领域的一个子集&#xff0c;通过模拟人脑神经元的连接方式&#xff0c;构建复杂的网络结构来处理和分析数据。然而&#xff0c;随着深度学习模型规模的不断扩大和复杂度的提高&#xff0c;其对计算资源的需求也呈指数级增长。因此&#…

远程连接Linux系统

图形化、命令行 对于操作系统的使用&#xff0c;有2种使用形式&#xff1a; 图形化页面使用操作系统 图形化&#xff1a;使用操作系统提供的图形化页面&#xff0c;以获得图形化反馈的形式去使用操作系统。 以命令的形式使用操作系统 命令行&#xff1a;使用操作系统提供的各…

OpenCV学习笔记(五)——图片的缩放、旋转、平移、裁剪以及翻转操作

目录 图像的缩放 图像的平移 图像的旋转 图像的裁剪 图像的翻转 图像的缩放 OpenCV中使用cv2.resize()函数进行缩放&#xff0c;格式为&#xff1a; resize_imagecv2.resize(image,(new_w,new_h),插值选项) 其中image代表的是需要缩放的对象&#xff0c;(new_w,new_h)表…

1950-2022年各区县逐年平均降水量数据

1950-2022年各区县逐年平均降水量数据 1、时间&#xff1a;1950-2022年 2、指标&#xff1a;省逐年平均降水量 3、范围&#xff1a;33省&#xff08;不含澳门&#xff09;、360地级市、2800个县 4、指标解释&#xff1a;逐年平均降水数据是指当年的日降水量的年平均值&…

Swift 入门学习:集合(Collection)类型趣谈-上

概览 集合的概念在任何编程语言中都占有重要的位置&#xff0c;正所谓&#xff1a;“古来聚散地&#xff0c;宿昔长荆棘&#xff1b;游人聚散中&#xff0c;一片湖光里”。把那一片片、一瓣瓣、一粒粒“可耐”的小精灵全部收拢、吸纳的井然有序、条条有理&#xff0c;怎能不让…

chrome高内存占用问题

chrome号称内存杀手不是盖的&#xff0c;不设设置的话&#xff0c;经常被它内存耗尽死机是常事。以下自用方法 1 自带的memory saver chrome://settings/performance PerformanceMemory Saver When on, Chromium frees up memory from inactive tabs. This gives active tab…

【工具】Git的24种常用命令

相关链接 传送门&#xff1a;>>>【工具】Git的介绍与安装<< 1.Git配置邮箱和用户 第一次使用Git软件&#xff0c;需要告诉Git软件你的名称和邮箱&#xff0c;否则无法将文件纳入到版本库中进行版本管理。 原因&#xff1a;多人协作时&#xff0c;不同的用户可…

K8S - 在任意node里执行kubectl 命令

当我们初步安装玩k8s &#xff08;master 带 2 nodes&#xff09; 时 正常来讲kubectl 只能在master node 里运行 当我们尝试在某个 node 节点来执行时&#xff0c; 通常会遇到下面错误 看起来像是访问某个服务器的8080 端口失败了。 原因 原因很简单 , 因为k8s的各个组建&…

Windows下同一电脑配置多个Git公钥访问不同的账号

前言 产生这个问题的原因是我在Gitee码云上有两个账号,为了方便每次不用使用http模式推拉代码,于是我就使用了ssh的模式,起初呢我用两台电脑分别连接两个账号,用起来也相安无事,近段时时间台式机在家里,我在外地出差了,就想着把ssh公钥同时添加到不同的账号里,结果却发现不能用…

软考高级:信息系统开发方法1(原型法、结构法等)概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

【NERF】入门学习整理(二)

【NERF】入门学习整理(二) 1. Hierarchicalsampling分层采样2. Loss定义(其实就是简单的均方差MSE)3. 隐式重建与显示重建1. Hierarchicalsampling分层采样 粗网络coarse,均匀采样64个点 缺点:如果仅使用粗网络会存在点位浪费和欠采样的问题,比比如空气中很多无效的点 精细…