KOOM原理

一.解决GC卡顿


为什么LeakCanary需要主动触发GC呢?LeakCanary监控泄漏利用了弱引用的特性,为Activity创建弱引用,当Activity对象变成弱可达时(没有强引用),弱引用会被加入到引用队列中,通过在Activity.onDestroy()后连续触发两次GC,并检查引用队列,可以判定Activity是否发生了泄漏。但频繁的GC会造成用户可感知的卡顿,为解决这一问题,我们设计了全新的监控模块,通过无性能损耗的内存阈值监控来触发镜像采集,具体策略如下:

具体策略如下:

  • Java堆内存突破阈值触发采集 - 90%
  • Java堆上涨速度突破阈值触发采集 - 两次检测时间间隔内增加350M直接dump
    • 如何获取堆信息通过Runtime.getRuntime()获取
      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
  • 处于高位heapThreshold,并且一直处于高位降的不明显
         heapThreshold取值如下
         maxMem >= 512 - 10 -> 0.8f
         maxMem >= 256 - 10 -> 0.85f
         else -> 0.9f
  • Java堆线程数突破阈值触发采集
    • 线程阈值默认值
      private val DEFAULT_THREAD_THRESHOLD by lazy {
          if (MonitorBuildConfig.ROM == "EMUI" &&
              Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
              450
          } else {
              750
          }
      }
    • 当前线程数量,读取"/proc/self/status"文件获取线程信息
      File("/proc/self/status").forEachLineQuietly { line ->
          ...
          when {
              ...
              line.startsWith("Threads") -> {
                  procStatus.thread = THREADS_REGEX.matchValue(line)
              }
          }
      }
  • 文件描述符数突破阈值触发采集 - 1000
    • 当前fd数量
      private fun getFdCount(): Int {
          return File("/proc/self/fd").listFiles()?.size ?: 0
      }

二.解决Dump hprof冻结app

Dump hprof是通过API Debug.dumpHprofData实现的,这个过程会**“冻结”**整个应用进程,造成数秒甚至数十秒内用户无法操作,这也是LeakCanary无法线上部署的最主要原因,如果能将这一过程优化至用户无感知,将会给OOM治理带来很大的想象空间。

面对这样一个问题,我们将其拆解,自然而然产生2个疑问:
1.为什么dumpHprofData会冻结app,虚拟机的实现原理是什么?
2.这个过程能异步吗?

我们来看dumpHprofData的虚拟机内部实现
art/runtime/hprof/hprof.cc

// If "direct_to_ddms" is true, the other arguments are ignored, and data is
// sent directly to DDMS.
// If "fd" is >= 0, the output will be written to that file descriptor.
// Otherwise, "filename" is used to create an output file.
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,
                                  gc::kGcCauseHprof,
                                  gc::kCollectorTypeHprof);
  ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);
  Hprof hprof(filename, fd, direct_to_ddms);
  hprof.Dump();
}

可以看到在dump前,通过ScopedSuspendAll(构造函数中执行SuspendAll)执行了暂停所有java线程的操作,以防止在dump的过程中java堆发生变化,当dump结束后通过ScopedSuspendAll析构函数进行ResumeAll。

解决了第一个问题,接下来看第二个问题,既然要冻结所有线程,子线程异步处理是没有意义的,那么在子进程中处理呢?Android的内核是定制过的Linux, 而Linux fork子进程有一个著名的COW(Copy-on-write,写时复制)机制,即为了节省fork子进程的内存消耗和耗时,fork出的子进程并不会copy父进程的内存,而是和父进程共享内存空间。那么如何做到进程隔离呢,父子进程只在发生内存写入操作时,系统才会分配新的内存为写入方保留单独的拷贝,这就相当于子进程保留了fork瞬间时父进程的内存镜像,且后续父进程对内存的修改不会影响子进程,想到这里我们豁然开朗。说干就干,我们写了一个demo来验证这个思路,

很快就遇到了棘手的新问题:dump前需要暂停所有java线程而子进程只保留父进程执行fork操作的线程,在子进程中执行SuspendAll触发暂停是永远等不到其他线程返回结果的(详见thread_list.cc中行SuspendAll的实现,这里不展开讲了),经过仔细分析SuspendAll的过程,我们发现,可以先在主进程执行SuspendAll,使ThreadList中保存的所有线程状态为suspend,之后fork,子进程共享父进程的ThreadList全局变量,子进程可以欺骗虚拟机,使其以为子进程全部线程已经完成了暂停操作,接下来子进程就可以愉快的dump hprof了,而父进程可以立刻执行ResumeAll恢复运行。

这里有一个小技巧,SuspendAll没有对外暴露Java层的API,我们可以通过C层间接暴露的art::Dbg::SuspendVM来调用,dlsym拿到“_ZN3art3Dbg9SuspendVMEv”的地址调用即可,ResumeAll同理,注意这个函数在android 11以后已经被去除了,需要另行适配。Android 7之后对linker做了限制(即dlopen系统库失效),快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制。
至此,我们完美解决了dump hprof冻结app的问题,用一张图总结:

1.ForkJvmHeapDumper  fork dump流程

            //topic 如何开辟子进程dump的 step1
            ForkJvmHeapDumper().run {
                dump(hprofFile.absolutePath)//开辟子进程dump
            }


public class ForkJvmHeapDumper extends HeapDumper {
    ...

    @Override
    public boolean dump(String path) {
        MonitorLog.i(TAG, "dump " + path);
        ...

        boolean dumpRes = false;
        try {
            MonitorLog.i(TAG, "before suspend and fork.");
            //topic 如何开辟子进程dump的 step3-1
            int pid = suspendAndFork();//pid为0,开辟的子进程,将去dump
            if (pid == 0) {
                // Child process
                Debug.dumpHprofData(path);
                exitProcess();//topic 如何开辟子进程dump的 step3-2
            } else if (pid > 0) {//主进程将会resumeAndWait
                // Parent process
                dumpRes = resumeAndWait(pid);//topic 如何开辟子进程dump的 step3-3
                MonitorLog.i(TAG, "notify from pid " + pid);
            }
        } catch (IOException e) {
            MonitorLog.e(TAG, "dump failed caused by " + e.toString());
            e.printStackTrace();
        }
        return dumpRes;
    }

    /**
     * Init before do dump. //topic 如何开辟子进程dump的 step2
     */
    private native void init();

    /**
     * Suspend the whole ART, and then fork a process for dumping hprof.
     *
     * @return return value of fork
     */
    private native int suspendAndFork();//topic 如何开辟子进程dump的 step3-1

    /**
     * Resume the whole ART, and then wait child process to notify.
     *
     * @param pid pid of child process.
     */
    private native boolean resumeAndWait(int pid);//topic 如何开辟子进程dump的 step3-3

    /**
     * Exit current process.
     */
    private native void exitProcess();//topic 如何开辟子进程dump的 step3-2
}

2.hrof_dump.h 代码分析

 
#ifndef KOOM_HPROF_DUMP_H
#define KOOM_HPROF_DUMP_H
 
#include <android-base/macros.h>
 
#include <memory>
#include <string>
 
namespace kwai {
    namespace leak_monitor {
 
        ...
 
        class HprofDump {
        public:
            //获取HprofDump实例
            static HprofDump &GetInstance();
 
            //初始化
            void Initialize();
 
            //SuspendAndFork
            pid_t SuspendAndFork();
 
            //ResumeAndWait
            bool ResumeAndWait(pid_t pid);
 
        private:
            HprofDump();
 
            ~HprofDump() = default;
 
            //https://blog.csdn.net/u011157036/article/details/45247965
            //有时候,进行类体设计时,会发现某个类的对象是独一无二的,没有完全相同的对象,也就是对该类对象做副本没有任何意义.
            //因此,需要限制编译器自动生动的拷贝构造函数和赋值构造函数.一般参用下面的宏定义的方式进行限制,代码如下:
            DISALLOW_COPY_AND_ASSIGN(HprofDump);
            //初始化完成
            bool init_done_;
            //api版本
            int android_api_;
 
 
            /**
             * Function pointer for ART <= Android Q
             * 方法指针 ART小于等于Android q的
             */
             //suspend vm的方法
            // art::Dbg::SuspendVM
            void (*suspend_vm_fnc_)();
 
            //resume vm的方法
            // art::Dbg::ResumeVM
            void (*resume_vm_fnc_)();
 
            /**
             * Function pointer for ART Android R
             * todo 方法指针 art android R的忽略先
             */
           
        };
 
    }  // namespace leak_monitor
}  // namespace kwai
#endif  // KOOM_HPROF_DUMP_H

3.hprof_dump.cpp 原理

#undef LOG_TAG
#define LOG_TAG "HprofDump"

using namespace kwai::linker;

namespace kwai {
    namespace leak_monitor {
        ......

        //初始化
        void HprofDump::Initialize() {
            if (init_done_ || android_api_ < __ANDROID_API_L__) {
                return;
            }

            //获取libart的handle手柄,把手
            void *handle = kwai::linker::DlFcn::dlopen("libart.so", RTLD_NOW);
            KCHECKV(handle)

            if (android_api_ < __ANDROID_API_R__) {
                //获取SuspendVMEv方法指针
                suspend_vm_fnc_ =
                        (void (*)()) DlFcn::dlsym(handle, "_ZN3art3Dbg9SuspendVMEv");
                KFINISHV_FNC(suspend_vm_fnc_, DlFcn::dlclose, handle)

                //获取ResumeVMEv方法指针,resume vm的方法
                resume_vm_fnc_ = (void (*)()) kwai::linker::DlFcn::dlsym(
                        handle, "_ZN3art3Dbg8ResumeVMEv");
                KFINISHV_FNC(resume_vm_fnc_, DlFcn::dlclose, handle)
            }
            if (android_api_ == __ANDROID_API_R__) {
                //R 的忽略先... 
            }
            DlFcn::dlclose(handle);
            init_done_ = true;
        }

        pid_t HprofDump::SuspendAndFork() {
            KCHECKI(init_done_)

            if (android_api_ < __ANDROID_API_R__) {
                suspend_vm_fnc_();      //suspend虚拟机
            }
            if (android_api_ == __ANDROID_API_R__) {
                 //R 的忽略先... 
            }

            pid_t pid = fork();         //fork子进程
            if (pid == 0) {             //如果是子进程
                // Set timeout for child process
                alarm(60);              //子进程60s之内退出
                prctl(PR_SET_NAME, "forked-dump-process");//设置子进程名字
            }
            return pid;
        }

        bool HprofDump::ResumeAndWait(pid_t pid) {
            KCHECKB(init_done_)     //检测init_done_是否true,如果不是true,return;

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值