Android修炼系列(34),内存监控技术方案(中)

·  阅读 1344

本系列将围绕下面几个方面来介绍内存监控方案:

  • FD 数量
  • 线程数量
  • 虚拟内存
  • java 堆
  • Native 内存

上一节介绍了 如何进行 FD 监控、线程数量监控,并从 koom 源码角度,详细介绍了如何监听 java, native 线程栈内存泄漏的问题。

本节将介绍如何进行虚拟内存和 java 堆内存的监控,并分别从 matrix 和 koom 源码出发,说说主流的两种 java 内存泄漏监听方式。

虚拟内存

当应用程序进行内存分配时,得到的是虚拟内存,只有真正去写这一内存块时,才会产生缺页中断,进而分配物理内存。虚拟内存的大小主要受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 调用的,这里就不再赘述了。

监控

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 的内存监控:

  1. so 大内存申请监控。

  2. 大图的申请监控。

  3. Native 内存泄漏监控。

本节完。

分类:
Android
分类:
Android
收藏成功!
已添加到「」, 点击更改