大厂的OOM优化和监控方案

大意了,又是 OOM ,Android 内存监控一定要注意这几点! 前言 性能优化是一个老生常谈的问题,也一直是Android中高级攻城狮进阶和深入的方向。 在性能优化的各种问题中,内存泄漏一直占有较重要的地位。内存泄漏的积累会造成内存溢出,进而导致程序崩溃,出现OOM问题。 针对这个问题,我们需要在上线前进行检测同时也要能够实现线上的监控。 方案一:线下性能监控 Android Studio自身提供的MAT等工具分析内存泄漏比较耗时,因而诞生了LeakCanary——这也是现在寻找内存泄漏问题最常用的工具。 LeakCanary 这个工具非常简单,依赖 Lea 阅读详情

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:juejin.cn/post/

7074762489736478757

e292659dcbc16ff581c9ce6e6b7f191e.png


一、前言

随着项目不断壮大,OOM (Out Of Memory)成为崩溃统计平台上的疑难杂症之一,大部分业务开发人员对于线上OOM问题一般都是暂不处理,一方面是因为OOM问题没有足够的log,无法在短期内分析解决,另一方面可能是忙于业务迭代、身心疲惫,没有精力去研究OOM的解决方案。

这篇文章将以线上OOM问题作为切入点,介绍常见的OOM类型、OOM的原理、大厂OOM优化黑科技、以及主流的OOM监控方案。

文章较长,请备好小板凳~

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

二、OOM问题分类

很多人对于OOM的理解就是Java虚拟机内存不足,但通过线上OOM问题分析,OOM可以大致归为以下3类:

  1. 线程数太多

  2. 打开太多文件

  3. 内存不足

接下来将分别围绕这三类问题进行展开分析~

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:https://github.com/YunaiV/onemall

三、线程数太多

3.1 报错信息

pthread_create (1040KB stack) failed: Out of memory

这个是典型的创建新线程触发的OOM问题

4c9e30e318f374221b878402b2bdc98d.png
oom1

3.2 源码分析

pthread_create触发的OOM异常,源码(Android 9)位置如下:androidxref.com/9.0.0_r3/xr…

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  ...
  pthread_create_result = pthread_create(...)
  //创建线程成功
  if (pthread_create_result == 0) {
      return;
  }
  //创建线程失败
  ...
  {
    std::string msg(child_jni_env_ext.get() == nullptr ?
        StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
        StringPrintf("pthread_create (%s stack) failed: %s",
                                 PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }
}

pthread_create里面会调用Linux内核创建线程,那什么情况下会创建线程失败呢?

查看系统对每个进程的线程数限制

cat /proc/sys/kernel/threads-max

4cc5187d1953947a982ade1976765129.png
线程数限制

不同设备的threads-max限制是不一样的,有些厂商的低端机型threads-max比较小,容易出现此类OOM问题。

查看当前进程运行的线程数

cat proc/{pid}/status

025a41759cdf2b8c7e90fb459b6af332.png
线程数

当线程数超过/proc/sys/kernel/threads-max中规定的上限时就会触发OOM。

既然系统对每个进程的线程数有限制,那么解决这个问题的关键就是尽可能降低线程数的峰值。

线程优化

回看两年前我写过一篇文章《面试官:今日头条启动很快,你觉得可能是做了哪些优化?》,虽然里面的内容有些已经过时,不过分析问题的思路还是可以借鉴的,记得当时对于线程优化只是一句话描述,今天这篇文章刚好可以做一个补充。

3.3.1 禁用 new Thread

解决线程过多问题,传统的方案是禁止使用new Thread,统一使用线程池,但是一般很难人为控制, 可以在代码提交之后触发自动检测,有问题则通过邮件通知对应开发人员。

不过这种方式存在两个问题:

  1. 无法解决老代码的new Thread;

  2. 对于第三方库无法控制。

3.3.2 无侵入性的new Thread 优化

Java层的Thread只是一个普通的对象,只有调用了start方法,才会调用native 层去创建线程,

所以理论上我们可以自定义Thread,重写start方法,不去启动线程,而是将任务放到线程池中去执行,为了做到无侵入性,需要在编译期通过字节码插桩的方式,将所有new Thread字节码都替换成new 自定义Thread。

对于字节码操作,在上一篇文章《ASM hook隐私方法调用,防止App被下架》已经详细介绍,本文不再过多解释。

步骤如下:

1、创建一个Thread的子类叫ShadowThread吧,重写start方法,调用自定义的线程池CustomThreadPool来执行任务;

public class ShadowThread extends Thread {

    @Override
    public synchronized void start() {
        Log.i("ShadowThread", "start,name="+ getName());
        CustomThreadPool.THREAD_POOL_EXECUTOR.execute(new MyRunnable(getName()));
    }

    class MyRunnable implements Runnable {

        String name;
        public MyRunnable(String name){
            this.name = name;
        }

        @Override
        public void run() {
            try {
                ShadowThread.this.run();
                Log.d("ShadowThread","run name="+name);
            } catch (Exception e) {
                Log.w("ShadowThread","name="+name+",exception:"+ e.getMessage());
                RuntimeException exception = new RuntimeException("threadName="+name+",exception:"+ e.getMessage());
                exception.setStackTrace(e.getStackTrace());
                throw exception;
            }
        }
    }
}

2、在编译期,hook 所有new Thread字节码,全部替换成我们自定义的ShadowThread,这个难度应该不大,按部就班,

我们先确认new Thread和new ShadowThread对应字节码差异,可以安装一个ASM Bytecode Viewer插件,如下所示

ed4c7cacebff79e26e98172e24aeedcb.png

通过字节码修改,你可以简单理解为做如下替换:

557c8a28bae036e641b76a11f43f0015.png

3、由于将任务放到线程池去执行,假如线程奔溃了,我们不知道是哪个线程出问题,所以自定义ShadowThread中的内部类MyRunnable 的作用是:在线程出现异常的时候,将异常捕获,还原它的名字,重新抛出一个信息更全的异常。

测试代码

private fun testThreadCrash() {
        Thread {
            val i = 9 / 0
        }.apply {
            name = "testThreadCrash"
        }.start()
    }

开启一个线程,然后触发奔溃,堆栈信息如下:

968cf831c5d95da058bd99051cec9901.png
线程堆栈

可以看到原本的new Thread已经被优化成了CustomThreadPool线程池调用,并且奔溃的时候不用担心找不到线程是哪里创建的,会还原线程名。

当然这种方式有一个小问题,应用正常运行的情况下,如果你想要收集所有线程信息,那么线程名可能不太准确,因为通过new Thread 去创建线程,已经被替换成线程池调用了,获取到的线程名是线程池中的线程的名字

数据对比

同个场景简单测试了一下new Thread优化前后线程数峰值对比:

线程数峰值(优化前)线程数峰值(优化后)降低最大线程数
33731423

对于不同App,优化效果会有一些不同,不过可以看到这个优化确实是有效的。

3.3.3 无侵入的线程池优化

随着项目引入的SDK越来越多,绝大部分SDK内部都会使用自己的线程池做异步操作,

线程池的参数如果设置不对,核心线程空闲的时候没有释放,会使整体的线程数量处于较高位置。

线程池几个参数:
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
  1. corePoolSize :核心线程数量。核心线程默认情况下即使空闲也不会释放,除非设置allowCoreThreadTimeOut为true。

  2. maximumPoolSize :最大线程数量。任务数量超过核心线程数,就会将任务放到队列中,队列满了,就会启动非核心线程执行任务,线程数超过这个限制就会走拒绝策略;

  3. keepAliveTime :空闲线程存活时间

  4. unit:时间单位

  5. workQueue:队列。任务数量超过核心线程数,就会将任务放到这个队列中,直到队列满,就开启新线程,执行队列第一个任务。

  6. threadFactory:线程工厂。实现new Thread方法创建线程

通过线程池参数,我们可以找到优化点如下:
  1. 限制空闲线程存活时间,keepAliveTime 设置小一点,例如1-3s;

  2. 允许核心线程在空闲时自动销毁

executor.allowCoreThreadTimeOut(true)

如何做呢?为了做到无侵入性,依然采用ASM操作字节码,跟new Thread的替换基本同理

在编译期,通过ASM,做如下几个操作:
  1. 将调用 Executors 类的静态方法替换为自定义 ShadowExecutors 的静态方法,设置executor.allowCoreThreadTimeOut(true);

  2. 将调用 ThreadPoolExecutor 类的构造方法替换为自定义 ShadowThreadPoolExecutor 的静态方法,设置executor.allowCoreThreadTimeOut(true);

  3. 可以在 Application 类的() 中调用我们自定义的静态方法 ShadowAsyncTask.optimizeAsyncTaskExecutor() 来修改 AsyncTask 的线程池参数,调用executor.allowCoreThreadTimeOut(true);

你可以简单理解为做如下替换:3b425f2e09651a7ba6c72c54d35315c0.png

详细代码可以参考 booster。

3.4 线程监控

假如线程优化后还存在创建线程OOM问题,那我们就需要监控是否存在线程泄漏的情况。

3.4.1 线程泄漏监控

主要监控native线程的几个生命周期方法:pthread_create、pthread_detach、pthread_join、pthread_exit。

  1. hook 以上几个方法,用于记录线程的生命周期和堆栈,名称等信息;

  2. 当发现一个joinable的线程在没有detach或者join的情况下,执行了pthread_exit,则记录下泄露线程信息;

  3. 在合适的时机,上报线程泄露信息。

linux线程中,pthread有两种状态joinable状态 和unjoinable状态 。joinable 状态下,当线程函数自己返回退出时或pthread_exit时 都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后 这些资源才会被释放,需要main函数或者其他线程去调用pthread_join函数。

具体代码可以参考:KOOM-thread_holder

3.4.2 线程上报

当监控到线程有异常的时候,我们可以收集线程信息,上报到后台进行分析。

收集线程信息代码如下:

private fun dumpThreadIfNeed() {

        val threadNames = runCatching { File("/proc/self/task").listFiles() }
            .getOrElse {
                return@getOrElse emptyArray()
            }
            ?.map {
                runCatching { File(it, "comm").readText() }.getOrElse { "failed to read $it/comm" }
            }
            ?.map {
                if (it.endsWith("\n")) it.substring(0, it.length - 1) else it
            }
            ?: emptyList()

        Log.d("TAG", "dumpThread = " + threadNames.joinToString(separator = ","))
    }

接下来介绍打开太多文件导致的OOM问题

四、打开太多文件

4.1 错误信息

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
Java.lang.OutOfMemoryError: Could not allocate JNI Env

这个问题跟系统、厂商关系比较大

4.2 系统限制

Android是基于Linux内核,/proc/pid/limits 描述着linux系统对每个进程的一些资源限制,

如下图是一台Android 6.0的设备,Max open files的限制是1024

8d8e65580f6a58c808a668acf54becee.png
maxopenfiles

如果没有root权限,可以通过ulimit -n命令查看Max open files,结果是一样的

ulimit -n

cc94aaa04c0f1867071feadf1d8f45ed.png
ulimit

Linux 系统一切皆文件,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面)

cd /proc/10654/fd

ls

de64626b34f0b4576fd5c2c6ed82eabc.png
fd

这些fd文件都是链接文件,通过 ls -l可以查看其对应的真实文件路径

74e252b5db58d2d8e0ae42ea89c85edb.png
fd2

当fd的数目达到Max open files规定的数目,就会触发Too many open files的奔溃,这种奔溃在低端机上比较容易复现。

知道了文件描述符这玩意后,看看怎么优化~

4.2 文件描述符优化

对于打开文件数太多的问题,盲目优化其实无从下手,总体的方案是监控为主。

通过如下代码可以查看当前进程的fd信息

private fun dumpFd() {
        val fdNames = runCatching { File("/proc/self/fd").listFiles() }
            .getOrElse {
                return@getOrElse emptyArray()
            }
            ?.map { file ->
                runCatching { Os.readlink(file.path) }.getOrElse { "failed to read link ${file.path}" }
            }
            ?: emptyList()

        Log.d("TAG", "dumpFd: size=${fdNames.size},fdNames=$fdNames")

    }

4.3 文件描述符监控

监控策略:当fd数大于1000个,或者fd连续递增超过50个,就触发fd收集,将fd对应的文件路径上报到后台。

这里模拟一个bug,打开一个文件多次不关闭,通过dumpFd,可以看到很多重复的文件名,进而大致定位到问题。

e15d1ecf38bc2a9d41b5bc89202341f0.png
fd count

当怀疑某个文件有问题之后,我们还需要知道这个文件在哪创建,是谁创建的,这个就涉及到IO监控~

4.4 IO监控

4.4.1 监控内容

监控完整的IO操作,包括open、read、write、close

open :获取文件名、fd、文件大小、堆栈、线程

read/write :获取文件类型、读写次数、总大小,使用buffer大小、读写总耗时

close :打开文件总耗时、最大连续读写时间

4.4.2 Java监控方案:

以Android 6.0 源码为例,FileInputStream 的调用链如下

java : FileInputStream -> IoBridge.open -> Libcore.os.open ->  
 BlockGuardOs.open -> Posix.open

Libcore.java是一个不错的hook点

package libcore.io;
public final class Libcore {
    private Libcore() { }

    public static Os os = new BlockGuardOs(new Posix());
}

我们可以通过反射获取到这个Os变量,它是一个接口类型,里面定义了open、read、write、close方法,具体实现在BlockGuardOs里面。

// 反射获得静态变量
Class<?> clibcore = Class.forName("libcore.io.Libcore");
Field fos = clibcore.getDeclaredField("os");

通过动态代理的方式,在它所有IO方法前后加入插桩代码来统计IO信息

// 动态代理对象
Proxy.newProxyInstance(cPosix.getClassLoader(), getAllInterfaces(cPosix), this);

beforeInvoke(method, args, throwable);
result = method.invoke(mPosixOs, args);
afterInvoke(method, args, result);

此方案缺点如下:

  • 性能差,IO调用频繁,使用动态代理和Java的字符串操作,导致性能较差,无法达到线上使用标准

  • 无法监控Native代码,这个也是比较重要的

  • 兼容性差:需要根据Android 版本做适配,特别是Android P的非公开API限制

4.4.3 Native监控方案

Native Hook方案的核心从 libc.so 中的这几个函数中选定 Hook 的目标函数

int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size); write_cuk
int close(int fd);

我们需要选择一些有调用上面几个方法的 library,例如选择libjavacore.so、libopenjdkjvm.so、libopenjdkjvm.so,可以覆盖到所有的 Java 层的 I/O 调用。

不同版本的 Android 系统实现有所不同,在 Android 7.0 之后,我们还需要替换下面这三个方法。

open64
__read_chk
__write_chk

native hook 框架目前使用比较广泛的是爱奇艺的xhook ,以及它的改进版,字节跳动的bhook。

具体的native IO监控代码,可以参考 Matrix-IOCanary,内部使用的是xhook框架。

关于IO涉及到的知识非常多,后面有时间可以单独整理一篇文章。

接下来看看最后一种OOM类型~

五、内存不足

5.1 堆栈信息

7e5dcf442a0c12f2d9c0d66f6672285e.png
内存不足

这种是最常见的OOM,Java堆内存不足,512M都不够玩~

发生此问题的大部分设备都是Android 7.0,高版本也有,不过相对较少。

5.2 重温JVM内存结构

JVM在运行时,将内存划分为以下5个部分

  1. 方法区:存放静态变量、常量、即时编译代码;

  2. 程序计数器:线程私有,记录当前执行的代码行数,方便在cpu切换到其它线程再回来的时候能够不迷路;

  3. Java虚拟机栈:线程私有,一个Java方法开始和结束,对应一个栈帧的入栈和出栈,栈帧里面有局部变量表、操作数栈、返回地址、符号引用等信息;

  4. 本地方法栈:线程私有,跟Java虚拟机栈的区别在于 这个是针对native方法;

  5. 堆:绝大部分对象创建都在堆分配内存

内存不足导致的OOM,一般都是由于Java堆内存不足,绝大部分对象都是在堆中分配内存,除此之外,大数组、以及Android3.0-7.0的Bitmap像素数据,都是存放在堆中。

Java堆内存不足导致的OOM问题,线上难以复现,往往比较难定位到问题,绝大部分设备都是8.0以下的,主要也是由于Android  3.0-7.0 Bitmap像素内存是存放在堆中 导致的。(可以参考之前一篇文章分析过其源码《面试官:简历上最好不要写Glide,不是问源码那么简单》)

基于这个结论,关于Java堆内存不足导致的OOM问题,优化方案主要是图片加载优化、内存泄漏监控 。

5.3 图片加载优化

5.3.1 常规的图片优化方式

常规的图片加载优化,依然可以参考两年前的一篇文章《面试官:简历上最好不要写Glide,不是问源码那么简单》, 文章核心内容大概如下:

  1. 分析了主流图片库Glide和Fresco的优缺点,以及使用场景;

  2. 分析了设计一个图片加载框架需要考虑的问题;

  3. 防止图片占用内存过多导致OOM的三个方式:软引用、onLowMemory、Bitmap 像素存储位置

这篇文章现在来看还是有点意义的,其中的原理部分还没过时,不过技术更新迭代,常规的优化方式已经不太够了,长远考虑,可以做图片自动压缩、大图自动检测和告警 。

5.3.2 无侵入性自动压缩图片

针对图片资源,设计师往往会追求高清效果,忽略图片大小,一般的做法是拿到图后手动压缩一下,这种手动的操作完全看个人修养。

无侵入性自动压缩图片,主流的方案是利用Gradle 的Task原理,在编译过程中,mergeResourcesTask 这个任务是将所以aar、module的资源进行合并,我们可以在mergeResourcesTask 之后可以拿到所有资源文件,具体做法:

  1. 在mergeResourcesTask这个任务后面,增加一个图片处理的Task,拿到所有资源文件;

  2. 拿到所有资源文件后,判断如果是图片文件,则通过压缩工具进行压缩,压缩后如果图片有变小,就将压缩过的图片替换掉原图。

可以简单理解如下:fbfa16304764e38561e9502f4c75a205.png

具体代码可以参考 McImage 这个库。

5.4  大图监控

5.3.2 自动压缩图片只是针对本地资源,而对于网络图片,如果加载的时候没有压缩,那么内存占用会比较大,这种情况就需要监控了。

5.4.1  从图片框架侧监控

很多App内部可能使用了多个图片库,例如Glide、Picasso、Fresco、ImageLoader、Coil,如果想监控某个图片框架, 那么我们需要熟读源码,找到hook点。

对于Glide,可以通过hook SingleRequest,它里面有个requestListeners,我们可以注册一个自己的监听,图片加载完做一个大图检测。

其它图片框架,同理也是先找到hook点,然后进行类似的hook操作就可以,代码可以参考:dokit-BigImgClassTransformer

5.4.2 从ImageView侧监控

5.4.1 是从图片加载框架侧监控大图,假如项目中使用到的图片加载框架太多,有些第三方SDK内部可能自己搞了图片加载,

这种情况下我们可以从ImageView控件侧做监控,监听setImageDrawable等方法,计算图片大小如果大于控件本身大小,debug包可以弹窗提示需要修改。

方案如下:

  1. 自定义ImageView,重写setImageDrawable、setImageBitmap、setImageResource、setBackground、setBackgroundResource这几个方法,在这些方法里面,检测Drawable大小;

  2. 编译期,修改字节码,将所有ImageView的创建都替换成自定义的ImageView;

  3. 为了不影响主线程,可以使用IdleHandler,在主线程空闲的时候再检测;

最终是希望当检测到大图的时候,debug环境能够弹窗提示开发进行修改,release环境可以上报后台。

debug如下效果:

60426bb81797d0ccae24a62ad6501a36.png
大图上报

当然这种方案有个缺点:不能获取到图片url。

图片优化告一段落,接下来看看内存泄漏~

5.5 内存泄漏监控演进

LeakCanary

关于内存泄漏,大家可能都知道LeakCanary,只要添加一个依赖

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1',

就能实现自动检测和分析内存泄漏,并发出一个通知显示内存泄漏详情信息。

LeakCanary只能在debug环境使用,因为它是在当前进程dump内存快照,Debug.dumpHprofData(path);会冻结当前进程一段时间,整个 APP 会卡死约5~15s,低端机上可能要几十秒的时间。

ResourceCanary

微信对LeakCanary做了一些改造,将检测和分析分离,客户端只负责检测和dump内存镜像文件,文件裁剪后上报到服务端进行分析。

具体可以看这篇文章Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余检测

KOOM

不管是LeakCanary 还是 ResourceCanary,他们都只能在线下使用,而线上内存泄漏监控方案,目前KOOM的方案比较完善,下面我将基于KOOM分析线上内存泄漏监控方案的核心流程。

5.6 线上内存泄漏监控方案

基于KOOM源码分析

5.6.1 检测时机
  1. 间隔5s检测一次

  2. 触发内存镜像采集的条件:

  • 当内存使用率达到80%以上

//->OOMMonitorConfig
      
      private val DEFAULT_HEAP_THRESHOLD by lazy {
        val maxMem = SizeUnit.BYTE.toMB(Runtime.getRuntime().maxMemory())
        when {
          maxMem >= 512 - 10 -> 0.8f
          maxMem >= 256 - 10 -> 0.85f
          else -> 0.9f
        }
      }
  • 两次检测时间内(例如5s内),内存使用率增加5%

5.6.2 内存镜像采集

我们知道LeakCanary检测内存泄漏,不能用于线上,是因为它dump内存镜像是在当前进程进行操作,会冻结App一段时间。

所以,作为线上OOM监控,dump内存镜像需要单独开一个进程。

整体的策略是:

虚拟机supend->fork虚拟机进程->虚拟机resume->dump内存镜像的策略。

dump内存镜像的源码如下:

//->ForkJvmHeapDumper
  
  public boolean dump(String path) {
    ...

    boolean dumpRes = false;
    try {
      //1、通过fork函数创建子进程,会返回两次,通过pid判断是父进程还是子进程
      int pid = suspendAndFork();
      
      MonitorLog.i(TAG, "suspendAndFork,pid="+pid);
      if (pid == 0) {
        //2、子进程返回,dump内存操作,dump内存完成,退出子进程
        Debug.dumpHprofData(path);
        exitProcess();
      } else if (pid > 0) {
        // 3、父进程返回,恢复虚拟机,将子进程的pid传过去,阻塞等待子进程结束
        dumpRes = resumeAndWait(pid);
        MonitorLog.i(TAG, "notify from pid " + pid);
      }
    }
    return dumpRes;
  }

注释1:父进程调用native方法挂起虚拟机,并且创建子进程;注释2:子进程创建成功,执行Debug.dumpHprofData,执行完后退出子进程;注释3:得知子进程创建成功后,父进程恢复虚拟机,解除冻结,并且当前线程等待子进程结束。

注释1源码如下:

// ->native_bridge.cpp

pid_t HprofDump::SuspendAndFork() {
  //1、暂停VM,不同Android版本兼容
  if (android_api_ < __ANDROID_API_R__) {
    suspend_vm_fnc_();
  }
  ...

  //2,fork子进程,通过返回值可以判断是主进程还是子进程
  pid_t pid = fork();
  if (pid == 0) {
    // Set timeout for child process
    alarm(60);
    prctl(PR_SET_NAME, "forked-dump-process");
  }
  return pid;
}

注释3源码如下:

//->hprof_dump.cpp

bool HprofDump::ResumeAndWait(pid_t pid) {
  //1、恢复虚拟机,兼容不同Android版本
  if (android_api_ < __ANDROID_API_R__) {
    resume_vm_fnc_();
  }
  ...
  int status;
  for (;;) {
    //2、waitpid,等待子进程结束
    if (waitpid(pid, &status, 0) != -1 || errno != EINTR) {
      //进程异常退出
      if (!WIFEXITED(status)) {
        ALOGE("Child process %d exited with status %d, terminated by signal %d",
              pid, WEXITSTATUS(status), WTERMSIG(status));
        return false;
      }
      return true;
    }
    return false;
  }
}

这里主要是利用Linux的waitpid函数,主进程可以等待子进程dump结束,然后再返回执行内存镜像文件分析操作。

5.6.3 内存镜像分析

前面一步已经通过Debug.dumpHprofData(path)拿到内存镜像文件,接下来就开启一个后台服务来处理

//->HeapAnalysisService
 
  override fun onHandleIntent(intent: Intent?) {
    ...
    kotlin.runCatching {
      //1、通过shark将hprof文件转换成HeapGraph对象
      buildIndex(hprofFile)
    }
    ...
    //2、将设备信息封装成json
    buildJson(intent)

    kotlin.runCatching {
      //3、过滤泄漏对象,有几个规制
      filterLeakingObjects()
    }
    ...
    kotlin.runCatching {
      // 4、gcRoot是否可达,判断内存泄漏
      findPathsToGcRoot()
    }
    ...

    //5、泄漏信息填充到json中,然后结束了
    fillJsonFile(jsonFile)


    //通知主进程内存泄漏分析成功
    resultReceiver?.send(AnalysisReceiver.RESULT_CODE_OK, null)

    //这个服务是在单独进程,分析完就退出
    System.exit(0);
  }

内存镜像分析的流程如下:

  1. 通过shark这个开源库将hprof文件转换成HeapGraph对象

  2. 收集设备信息,封装成json,现场信息很重要

  3. filterLeakingObjects:过滤出泄漏的对象,有一些规制,例如已经destroyed和finished的activity、fragment manager为空的fragment、已经destroyed的window等。

  4. findPathsToGcRoot:内存泄漏的对象,查找其到GcRoot的路径,通过这一步就可以揪出内存泄漏的原因

  5. fillJsonFile:格式化输出内存泄漏信息

小结

线上Java内存泄漏监控方案分析,这里小结一下:

  1. 挂起当前进程,然后通过fork创建子进程;

  2. fork会返回两次,一次是子进程,一次是父进程,通过返回的pid可以判断是子进程还是父进程;

  3. 如果是父进程返回,则通过resumeAndWait恢复进程,然后当前线程阻塞等待子进程结束;

  4. 如果子进程返回,通过Debug.dumpHprofData(path)读取内存镜像信息,这个会比较耗时,执行结束就退出子进程;

  5. 子进程退出,父进程的resumeAndWait就会返回,这时候就可以开启一个服务,后台分析内存泄漏情况,这块跟LeakCanary的分析内存泄漏原理基本差不多。

不画图了,结合源码看应该可以理解。

5.7 native内存泄漏监控

对于Java内存泄漏监控,线下我们可以使用LeakCanary、线上可以使用KOOM,而对于native内存泄漏应该如何监控呢?

方案如下:

首先要了解native层 申请内存的函数:malloc、realloc、calloc、memalign、posix_memalign释放内存的函数:free

  1. hook申请内存和释放内存的函数

f351e43f0a1f10703ff999e647e49644.png
hook申请内存和释放内存的函数

分配内存的时候,收集堆栈、内存大小、地址、线程等信息,存放到map中,在释放内存的时候从map中移除。

0f30142c2e80a6fb16607ee143a4cb9a.png
从map中移除

那怎么判断native内存泄漏呢?

  • 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」

  • 获取到不可达的内存块的地址后,可以从我们的Map中获取其堆栈、内存大小、地址、线程等信息。

具体实现可以参考:koom-native-leak

总结

本文从线上OOM问题入手,介绍了OOM原理, 以及OOM优化方案和监控方案,基本上都是大厂开源出来的比较成熟的方案:

  1. 对于pthread_create OOM问题,介绍了无侵入性的new Thread优化、无侵入性的线程池优化、以及线程泄漏监控;

  2. 对于文件描述符过多问题,介绍了原理以及文件描述符监控方案、IO监控方案;

  3. 对于Java内存不足导致的OOM、介绍了无侵入性图片自动压缩方案、两种无侵入性的大图监控方案、Java内存泄漏监控的线下方案和线上方案、以及native内存泄漏监控方案。

大厂对外开源的技术非常多,但不一定最优,我们在学习过程中可以多加思考, 例如线程优化,booster 对于new Thread的优化只是设置了线程名,有助于分析问题,而经过我的猜想和验证,通过字节码插桩,将new Thread无侵入性替换成线程池调用,才是真正意义上的线程优化。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

0468ff7cbab21baf378ec986911ab312.png

已在知识星球更新源码解析如下:

e5c543feb6166399b6fc79ec46c4d19c.png

525fdbb2a2e8940427f6650dd78563b1.png

832d0ed1b134299a31717c86bc4aa541.png

44a174fb8808fc7c39a3dfa3936d5719.png

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
AI辅助专利撰写实战:用Codex 30分钟生成发明专利初稿 这次我们来看一个用 Codex 从零开始撰写发明专利的实战项目。对于技术研发人员、产品经理或初创团队来说,专利申请是保护创新成果、构建技术壁垒的关键一步。然而,撰写一份高质量的专利申请书,尤其是权利要求书和说明书,往往需要专业的法律和技术语言,耗时耗力。这个项目演示了如何利用 AI 工具 Codex,在短时间内生成一份结构完整、逻辑清晰、具备授权潜力的专利申请文件初稿。 整个过程的核心不是让 AI 完全替代专利代理人,而是将其作为强大的“技术翻译”和“内容生成”助手。它能将你的技术构思、流程图、核心算法或产 阅读详情

相关推荐

ReportStudio入门教程(四十一) - 固定报表列宽

我们继续上一篇博客,让他看起来

什么是江湖? 3518

jmeter学习指南之OOM和监听器使用

jmeter视频:https://edu.51cto.com/course/14305.htmlOOM在执行压测的时候,有时候会遇到 OutOfMemoryError 内存溢出问题。可以通过调整 JVM 内存参数进行优化,是在 jmeter.bat 启动文件中配置的。以文本方式打开 jmeter.bat,修改以下配置项:调整堆内存 HEAP 的大小:将默认的 set HEAP...

weixin_34050519的博客 222

从仿真到实机:四足机器人控制算法的全流程验证框架

本文系统阐述了四足机器人从仿真到实机的全流程验证框架。针对仿真与实机环境的巨大差异,文章详细介绍了在MuJoCo等高保真仿真环境中的建模、算法迭代与压力测试方法,并重点解析了通过硬件抽象层和中间件搭建通往实机的关键桥梁。最后,提供了实机部署的安全清单与调试策略,旨在帮助开发者构建可重复、风险可控的算法验证闭环,提升算法在真实世界中的鲁棒性与可靠性。

lilh34434的博客 384

error: undefined reference to __write_chk错误

Android集成implementation 'com.github.pqpo:SmartCropper:v2.1.1'报错; 错误信息 error: undefined reference to '__write_chk' 原因 Android NDK版本过高,应该是高版本的优点问题 解决方法: 既然使高本版问题,那就降低 Android NDK的版本,我这里选择19版本,报错消失; 分割线 我首先尝试的使20版本,底层调用opencv等native方法的,就需要下载Android NDK; ...

Primer5 3463

Adaboost入门教程——最通俗易懂的原理介绍(图文实例)

Adaboost入门教程——最通俗易懂的原理介绍(图文实例)

Max的杂物仓 11万+

实战指南:在UE5中高效集成Entt(ECS)框架

本文提供了一份在虚幻引擎5中高效集成Entt ECS框架的实战指南。针对UE5项目在处理大规模动态实体时的性能瓶颈,详细讲解了如何通过数据驱动的ECS架构进行优化。内容包括前期环境准备、核心构建步骤、创建首个ECS子系统以及与UE原生系统的数据同步技巧,旨在帮助开发者快速掌握集成方法,提升游戏逻辑的执行效率。

weixin_29002209的博客 158

线上服务内存OOM问题定位三板斧

线上服务内存OOM的问题,是最难定位的问题,不过归根结底,最常见的原因:本身资源不够 申请的太多 资源耗尽某服务器上部署了Java服务一枚,出现了OutOfMemoryError,请问有可能是什么原因,问题应该如何定位? 不妨设服务进程PID为10765(没错,就是CPU占用高的那个进程。解决思路 Java服务OOM,最常见的原因为:有可能是内存分配确实过小,而正常业务使用了大量内存 某一个对象

******* ▄︻┻┳═一 ******* 3792

20个闭环渗透测试项目:从漏洞发现到简历突围

渗透测试不仅是工具使用,更是对系统设计缺陷与实现偏差的深度推演。理解OWASP Top 10、Burp Suite等基础能力只是起点,真正具备工程价值的是能复现、可验证、可协作的完整攻击链。本文聚焦真实业务场景下的SSRF协议绕过、JWT签名机制缺陷、Redis未授权利用、Kong网关配置风险及etcd存储结构分析等核心热词,揭示漏洞本质在于架构决策与配置偏差的叠加态。适用于红队实战、甲方安全建设与乙方渗透审计等多类岗位,帮助从业者将技术动作升维为技术叙事。

weixin_32234493的博客 415

java 如何定位oom

项目方案:如何定位Java中的OOM问题 背景 在Java应用程序开发中,OutOfMemoryError(OOM)是一个常见的问题,可能导致应用程序崩溃或者性能下降。定位OOM问题并解决它是非常重要的。本文将提出一种项目方案来帮助定位Java中的OOM问题。 方案 1. 基本思路 监控应用程序的内存使用情况。 当发生O...

weixin_41244679的博客 274

2025年9月GESP(C++四级):排兵布阵

作为将军,你自然需要合理地排兵布阵。地图可以视为n行m列的网格,适合排兵的网格以 1 标注,不适合排兵的网格以 0 标注。现在你需要在地图上选择一个矩形区域排兵,这个矩形区域内不能包含不适合排兵的网格。请问可选择的矩形区域最多能包含多少网格?

王老师青少年编程 1385

服务部署之配置WSUS更新服务(基于Windows配置 Server 2022)

服务部署之配置WSUS更新服务(基于Windows配置 Server 2022)

2301_77192876的博客 2215

深入探索Android卡顿优化(上)

此外,如果你需要分析Native 函数的调用,请使用Android 5.0 新增的[Simpleperf](()性能分析工具,它利用了 CPU 的性能监控单元(PMU)提供的硬件 perf 事件。使用 Simpleperf 可以看到所有的 Native 代码的耗时,对一些 Android 系统库的调用,在分析问题时有比较大的帮助,例如分析加载 dex、verify class 的耗时等等。此外,在 Android Studio 3.2 中的 Profiler 也直接支持了 Simpleper(SampleN

m0_54851114的博客 2429

USLE模型(1.2):LS因子的计算

基于USLE的甘南川西北土壤侵蚀研究。准备好30米分辨率的dem。

所有专家都曾始于新手 3481

【性能优化】大厂OOM优化和监控方案

作者:蓝师傅 一、前言 随着项目不断壮大,OOM(Out Of Memory)成为奔溃统计平台上的疑难杂症之一,大部分业务开发人员对于线上OOM问题一般都是暂不处理,一方面是因为OOM问题没有足够的log,无法在短期内分析解决,另一方面可能是忙于业务迭代、身心疲惫,没有精力去研究OOM的解决方案。 这篇文章将以线上OOM问题作为切入点,介绍常见的OOM类型、OOM的原理、大厂OOM优化黑科技、以及主流的OOM监控方案。 文章较长,请备好小板凳~ 二、OOM问题分类 很多人对于OOM的理解就是Java虚拟.

1 814

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

大厂干掉 OOM 的套路,不过如此

文章来源:https://c1n.cn/5ug0H目录前言OOM 问题分类线程数太多打开太多文件内存不足总结前言随着项目不断壮大,OOM(Out Of Memory)成为奔溃统计平台上的疑难杂症之一。大部分业务开发人员对于线上 OOM 问题一般都是暂不处理:一方面是因为 OOM 问题没有足够的 log,无法在短期内分析解决。另一方面可能是忙于业务迭代、身心疲惫,没有精力去...

石杉的架构笔记 484

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

大厂干掉 OOM 的套路,你知道几个 !

点击关注公众号,Java干货及时送达????文章来源:https://c1n.cn/5ug0H目录前言OOM 问题分类线程数太多打开太多文件内存不足总结前言随着项目不断壮大,OOM(Out Of Memory)成为奔溃统计平台上的疑难杂症之一。大部分业务开发人员对于线上 OOM 问题一般都是暂不处理:一方面是因为 OOM 问题没有足够的 log,无法在短期内分析解决。另一方面可...

这个时代,作为程序员可能要学习小程序 579

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

终面倒计时5分钟:应届生用Arthas破解OOM困局,P8考官紧盯FullGC日志

在终面最后5分钟,面试官抛出一道棘手的性能问题:一个Java应用在高并发场景下出现频繁FullGC,甚至触发OOM(内存溢出)。应届生迅速利用Arthas工具对运行时JVM进行诊断,通过分析堆内存使用情况和线程状态,成功定位到内存泄漏的根本原因。面对考官步步紧逼的FullGC日志分析问题,应届生冷静应对,最终提出优化方案,赢得了P8考官的高度认可。

itAred的博客 520

【JVM调优】如何进行JVM调优?一篇文章就够了!

深入解读JVM性能的监控、定位和调优方案,阐述jps/stat/jstack、MAT等常用性能分析工具的使用,提出JVM参数、内存溢出、内存泄漏、CPU飙升、GC频繁等实际场景下JVM调优的方案。

种一棵树最好的时间是十年前,其次是现在 4万+

JVM 性能监控与调优篇 概述篇

概述篇 一、大厂面试题 提前了解一下要学什么 支付宝: 支付宝三面:JVM性能调优都做了什么? 小米: 有做过JVM内存优化吗? 从SQL、JVM、架构、数据库四个方面讲讲优化思路 蚂蚁金服: JVM的编译优化 jvm性能调优都做了什么 JVM诊断调优工具用过哪些? 二面:jvm怎样调优,堆内存、栈空间设置多少合适 三面:JVM相关的分析工具使用过的有哪些?具体的性能调优步骤如何 阿里: 如何进行JVM调优?有哪些方法? 如何理解内存泄漏问题?有哪些情况会导致内存泄漏?如何解决? 字节跳动:

weixin_43967582的博客 466

互联网大厂Java面试深度解析:从音视频场景到高并发微服务架构

大厂面试的本质是“用技术解决不确定的业务问题”。本文所有问题均来自真实音视频中台故障复盘。掌握。

qq_54337203的博客 377

OOM!又是OOM!Android 内存监控一定要注意!

前言 性能优化是一个老生常谈的问题,也一直是Android中高级攻城狮进阶和深入的方向。 在性能优化的各种问题中,内存泄漏一直占有较重要的地位。 内存泄漏的积累会造成内存溢出,进而导致程序崩溃,出现OOM问题。 针对这个问题,我们需要在上线前进行检测同时也要能够实现线上的监控。 方案一:线下性能监控 Android Studio自身提供的MAT等工具分析内存泄漏比较耗时,因而诞生了LeakCanary——这也是现在寻找内存泄漏问题最常用的工具。 LeakCanary 这个工具非常简单,依赖 LeakCana

BUGgogogo的博客 526
上一篇: 官宣了!JetBrains IDE 全新默认 UI,超级好看,三步激活!
下一篇: 从5秒优化到1秒,系统飞起来了...
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值