本系列将围绕下面几个方面来介绍内存监控方案:
- FD 数量
- 线程数量
- 虚拟内存
- java 堆
- Native 内存
上一节介绍了 如何进行 FD 监控、线程数量监控,并从 koom 源码角度,详细介绍了如何监听 java, native 线程栈内存泄漏的问题。
虚拟内存
当应用程序进行内存分配时,得到的是虚拟内存,只有真正去写这一内存块时,才会产生缺页中断,进而分配物理内存。虚拟内存的大小主要受CPU架构及内核的限制。
一般 32 位的 CPU 架构,其地址空间最大为 4GB,arm64 架构,其地址空间为 512GB。对于虚拟内存的使用状态,我们可以通过 /process/pid/status 的 VmSize 字段获。获取方法与上一节说的,native 线程数量 Threads 字段一样:
File(String.format("/proc/%s/status", Process.myPid())).forEachLine { line ->
when {
line.startsWith("VmSize") -> {
Log.d("mjzuo", line)
}
}
}
复制代码
如果要进一步分析的话,可读取 /process/pid/smaps,这里记录了进程中所有的虚拟内存分配情况。当然我们也可以直接执行命令拿到当前的 smaps 文件:
# [com.blog.a]:包名, [22082]:进程ID
adb shell "run-as com.blog.a cat /proc/22082/smaps" > smaps.txt
复制代码
因为内容可读性太差,通常我们都会使用 py 脚本进行排序一下,部分原始内容见下:
7d4a434000-7d4a435000 rw-p 00010000 fe:00 4380 /system/vendor/lib64/libqdMetaData.so
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Anonymous: 4 kB
AnonHugePages: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd wr mr mw me ac
复制代码
以其中 libqdMetaData.so 在此进程中的映射信息为例:Size 表示其被分配的线性地址空间大小(即虚拟内存);Rss 表示占用的物理内存大小;Pss 表示占用的物理内存含公摊,并有关系:
Rss = Shared_Clean + Shared_Dirty + Private_Clean + Private_Dirty
复制代码
监控
以 matrix 为例:开启了一个 Matrix.GCSST 线程,定时检查 VmSize 大小是否超过阈值,默认 3 分钟,这个阈值 mCriticalVmSizeRatio 自己定义:
if (vmSize > 4L * 1024 * 1024 * 1024 * mCriticalVmSizeRatio)
复制代码
堆内存
Java 堆的大小是系统为应用程序设置的,我们可通过设置 AndroidManifest 中的 application.largeHeap 属性来获取更大的堆空间限制。而且我们能直接通过 Runtime 接口来获取一些堆内存状态,来配合内存快照排查问题:
javaHeap.max = Runtime.getRuntime().maxMemory()
javaHeap.total = Runtime.getRuntime().totalMemory()
javaHeap.free = Runtime.getRuntime().freeMemory()
javaHeap.used = javaHeap.total - javaHeap.free
javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max
复制代码
关于虚拟机堆栈的知识点,前面的博客也写了些,分别介绍了:类是如何被加载的,对象是如何被分配和回收的,方法是如何被 JVM 调用的,这里就不再赘述了。
-
扫描二维码关注公众号,回复: 13771610 查看本文章
监控
Java 堆区的内存有虚拟机代为申请和释放,我们无需关心。我们要关心的是如何避免内存泄露。下面介绍两种监控方案:
方案1
原理:在 Activity onDestroy 时,将 activity 封装成弱引用对象加入到队列中,并创建哨兵对象,随后手动 GC, 在哨兵对象被回收时,遍历队列内 activity 是否也被回收,如果未被回收,则存在泄漏,最后 dump 内存快照。
创建哨兵对象的目的,是因为手动 GC 并不能保证 JVM 会立即进行垃圾回收,这个时机是由虚拟机控制的,虚拟机会在合适的时机进行垃圾回收。
下面以 matrix 代码为例:
@Override
public void init(Application app, PluginListener listener) {
super.init(app, listener);
...
// 初始化监听,并创建 mHandlerThread("matrix_res")
// 并设置jump信息的mode=DumpMode.MANUAL_DUMP
mWatcher = new ActivityRefWatcher(app, this);
}
@Override
public void start() {
super.start();
...
mWatcher.start(); // 开始监控
}
@Override
public void start() {
stopDetect();
final Application app = mResourcePlugin.getApplication();
if (app != null) {
// 监听生命周期
app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
// 1分钟后执行 RetryableTask.Status status = task.execute()
// 执行 RetryableTask#execute()
// status == RetryableTask.Status.RETRY 轮询
scheduleDetectProcedure();
}
}
复制代码
前后台切换时,更新定时间隔。
public void onForeground(boolean isForeground) {
if (isForeground) {
// 前台时定时间隔1分钟
mDetectExecutor.setDelayMillis(mFgScanTimes);
... // 停止当前任务, 重新计时检查,并清空 failedAttempts
} else {
// 后台定时间隔20分钟
mDetectExecutor.setDelayMillis(mBgScanTimes);
}
}
复制代码
RetryableTask#execute():
// 如果当前还没有 onDestroy 的 Activity,则阻塞当前线程
if (mDestroyedActivityInfos.isEmpty()) {
synchronized (mDestroyedActivityInfos) {
try {
while (mDestroyedActivityInfos.isEmpty()) {
// 阻塞并释放锁
mDestroyedActivityInfos.wait();
}
}
...
}
// 并返回 RETRY,mHandlerThread 定时轮询
return Status.RETRY;
}
复制代码
当监听到 Activity 生命周期 onActivityDestroyed:
@Override public void onActivityDestroyed(Activity activity) {
// 将 onDestroy 的 Activity 都收集起来
pushDestroyedActivityInfo(activity);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
triggerGc(); // 并间隔两秒,手动触发一次 GC.
}
}, 2000);
}
复制代码
pushDestroyedActivityInfo(Activity):
private void pushDestroyedActivityInfo(Activity activity) {
...
// 将 onDestroy 的Activity 信息包装起来,并放入 ConcurrentLinkedQueue
mDestroyedActivityInfos.add(destroyedActivityInfo);
// 唤醒 matrix_res 线程
synchronized (mDestroyedActivityInfos) {
mDestroyedActivityInfos.notifyAll();
}
}
复制代码
RetryableTask#execute(): 老版本是采用哨兵对象来判断是否执行了 GC:
// 这是哨兵对象,用来检查 jvm 是否执行了 gc
final WeakReference<Object[]> sentinelRef = new WeakReference<>(new Object[1024 * 1024]); // alloc big object
triggerGc(); // 手动触发gc
if (sentinelRef.get() != null) {
return Status.RETRY;
}
复制代码
RetryableTask#execute(): 新版本直接3次调用 GC 方法:
triggerGc(); // 手动触发gc, sleep 1s
triggerGc();
triggerGc();
复制代码
RetryableTask#execute():
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
// 遍历onDestroy集合
while (infoIt.hasNext()) {
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
// 手动触发gc
triggerGc();
// 如果activity的弱引用没有了,则说明被回收了,没有泄漏
if (destroyedActivityInfo.mActivityRef.get() == null) {
infoIt.remove();
continue;
}
++destroyedActivityInfo.mDetectedCount;
// 重复检查mMaxRedetectTimes次,如果activity还没有被回收,则按照泄漏处理
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
&& !mResourcePlugin.getConfig().getDetectDebugger()) {
destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
triggerGc();
continue;
}
...
triggerGc();
// 执行ManualDumpProcessor#process
// 开启前台通知 Notification
// 注意其他mode: 会jump 内存快照,并解析hprof信息
if (mLeakProcessor.process(destroyedActivityInfo)) {
infoIt.remove();
}
}
复制代码
优点:判断内存泄漏比较准确;
缺点:手动 GC 存在性能开销。
方案2
原理:可以直接定时判断当前内存是否达到阈值,如果连续多次都达到阈值,并每次内从占用更高,则触发内存 dump。
下面以 KOOM 为例,原理一致,但标准略有不同,代码见下:
override fun startLoop(clearQueue: Boolean, postAtFront: Boolean, delayMillis: Long) {
...
// 初始化后,会开启线程 mLoopRunnable,并15s轮询一次
super.startLoop(clearQueue, postAtFront, delayMillis)
getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)
}
复制代码
LoopMonitor#startLoop:
open fun startLoop(
clearQueue: Boolean = true,
postAtFront: Boolean = false,
delayMillis: Long = 0L
) {
...
getLoopHandler().postDelayed(mLoopRunnable, delayMillis)
...
}
private val mLoopRunnable = object : Runnable {
override fun run() {
// 执行call 方法,并判断返回值来决定是否中断轮询
if (call() == LoopState.Terminate) {
return
}
...
getLoopHandler().removeCallbacks(this)
// 15s轮询一次
// OOMMonitor 重写 getLoopInterval=OOMMonitorConfig.mLoopInterval,默认15s
getLoopHandler().postDelayed(this, getLoopInterval())
}
}
复制代码
OOMMonitor#call:
override fun call(): LoopState {
// 目前仅支持android 5.0 - 11
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.S
) {
return LoopState.Terminate
}
...
return trackOOM() // 检查堆内存
}
复制代码
OOMMonitor#trackOOM:
private fun trackOOM(): LoopState {
SystemInfo.refresh() // 获取当前堆内存情况
mTrackReasons.clear() // 清空track集合
for (oomTracker in mOOMTrackers) {
if (oomTracker.track()) { // 对于 HeapOOMTracker 来说,负责堆内存泄漏条件后,添加集合
mTrackReasons.add(oomTracker.reason())
}
}
if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {
... // dump 信息
return LoopState.Terminate // 停止轮询
}
return LoopState.Continue // 继续轮询
}
复制代码
HeapOOMTracker#track:
override fun track(): Boolean {
val heapRatio = SystemInfo.javaHeap.rate // 或者堆内存使用率
// 泄漏条件:连续 3 次超过设置阈值,并每次都不低于(上次内存使用率-5%)
// 则认为内存居高不下,存在泄漏
if (heapRatio > monitorConfig.heapThreshold
&& heapRatio >= mLastHeapRatio - HEAP_RATIO_THRESHOLD_GAP) {
mOverThresholdCount++
} else {
// 如果不符合条件,则清空Count,Ratio,重新统计
reset()
}
mLastHeapRatio = heapRatio
// 默认 maxOverThresholdCount = 3次
return mOverThresholdCount >= monitorConfig.maxOverThresholdCount
}
复制代码
优点: 性能对用户体验影响很小。
缺点: 对于检测泄漏不太准确,存在误报情况。
下节会从三个角度介绍 Native 的内存监控:
-
so 大内存申请监控。
-
大图的申请监控。
-
Native 内存泄漏监控。
本节完。