KOOM 解决Dump hprof冻结app原理

解决Dump hprof冻结app
Dump hprof是通过虚拟机提供的API 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.hprof_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 {

        // todo What caused the GC? gc原因
        enum GcCause {
            // Invalid GC cause used as a placeholder.用作占位符的GC原因无效。
            kGcCauseNone,
            // GC triggered by a failed allocation. Thread doing allocation is blocked
            // waiting for GC before
            // retrying allocation.
            //由失败的分配触发的GC。执行分配的线程在重试分配之前被阻止,等待GC。
            kGcCauseForAlloc,
            // A background GC trying to ensure there is free memory ahead of allocations.
            //后台GC试图确保在分配之前有可用内存。
            kGcCauseBackground,
            // An explicit System.gc() call.
            //调用System.gc()导致的gc
            kGcCauseExplicit,
            // GC triggered for a native allocation when NativeAllocationGcWatermark is
            // exceeded.
            // (This may be a blocking GC depending on whether we run a non-concurrent
            // collector).
            //当超过NativeAllocationGcWatermark时,为本机分配触发GC。
            // (这可能是阻塞GC,具体取决于我们是否运行非并发收集器)。
            kGcCauseForNativeAlloc,
            // GC triggered for a collector transition.
            //为收集器转换触发GC。
            kGcCauseCollectorTransition,
            // Not a real GC cause, used when we disable moving GC (currently for
            // GetPrimitiveArrayCritical).
            //不是真正的GC原因,在禁用移动GC时使用(当前用于GetPrimitiveArrayCritical)。
            kGcCauseDisableMovingGc,
            // Not a real GC cause, used when we trim the heap.
            //不是真正的GC原因,在修剪堆时使用。
            kGcCauseTrim,
            // Not a real GC cause, used to implement exclusion between GC and
            // instrumentation.
            //不是真正的GC原因,用于实现GC和仪器仪表。
            kGcCauseInstrumentation,
            // Not a real GC cause, used to add or remove app image spaces.
            //不是真正的GC原因,用于添加或删除应用程序图像空间。
            kGcCauseAddRemoveAppImageSpace,
            // Not a real GC cause, used to implement exclusion between GC and debugger.
            //不是真正的GC原因,用于在GC和调试器之间实现排除
            kGcCauseDebugger,
            // GC triggered for background transition when both foreground and background
            // collector are CMS.
            //当前台和后台采集器均为CMS时,为后台转换触发GC。
            kGcCauseHomogeneousSpaceCompact,
            // Class linker cause, used to guard filling art methods with special values.
            //类链接器原因,用于保护使用特殊值填充艺术方法。
            kGcCauseClassLinker,
            // Not a real GC cause, used to implement exclusion between code cache
            // metadata and GC.
            //不是真正的GC原因,用于实现代码缓存元数据和GC之间的排除
            kGcCauseJitCodeCache,
            // Not a real GC cause, used to add or remove system-weak holders.
            //不是真正的GC原因,用于添加或删除系统弱保持器。
            kGcCauseAddRemoveSystemWeakHolder,
            // Not a real GC cause, used to prevent hprof running in the middle of GC.
            //不是一个真正的GC原因,用于防止HPROF在GC的中间运行
            kGcCauseHprof,
            // Not a real GC cause, used to prevent GetObjectsAllocated running in the
            // middle of GC.
            //不是真正的GC原因,用于防止GetObjectsLocated在GC的中间部分。
            kGcCauseGetObjectsAllocated,
            // GC cause for the profile saver.
            //配置文件保护程序的GC原因。
            kGcCauseProfileSaver,
            // GC cause for running an empty checkpoint.
            //GC导致运行空检查点。
            kGcCauseRunEmptyCheckpoint,
        };

        // 可以执行哪些类型的集合。Which types of collections are able to be performed.
        enum CollectorType {
            // No collector selected.
            //无类别collector
            kCollectorTypeNone,
            //非并发标记清除。
            // Non concurrent mark-sweep.
            kCollectorTypeMS,
            //并发标记清除。
            // Concurrent mark-sweep.
            kCollectorTypeCMS,
            //半空间/标记-清除混合,可实现压缩。
            // Semi-space / mark-sweep hybrid, enables compaction.
            kCollectorTypeSS,
            //堆修剪收集器,不做任何实际的收集。
            // Heap trimming collector, doesn't do any actual collecting.
            kCollectorTypeHeapTrim,
            //一个(主要是)并发复制收集器
            // A (mostly) concurrent copying collector.
            kCollectorTypeCC,
            //并发复制收集器的后台压缩。
            // The background compaction of the concurrent copying collector.
            kCollectorTypeCCBackground,
            //仪表临界区假收集器。
            // Instrumentation critical section fake collector.
            kCollectorTypeInstrumentation,
            //用于添加或删除应用程序图像空间的假收集器。
            // Fake collector for adding or removing application image spaces.
            kCollectorTypeAddRemoveAppImageSpace,
            //用于在 GC 和调试器之间实现排除的假收集器。
            // Fake collector used to implement exclusion between GC and debugger.
            kCollectorTypeDebugger,
            // A homogeneous space compaction collector used in background transition
            // when both foreground and background collector are CMS.
            //当前景和背景收集器都是 CMS 时,用于背景转换的同构空间压缩收集器。
            kCollectorTypeHomogeneousSpaceCompact,
            // Class linker fake collector.
            ///类链接器假收集器。
            kCollectorTypeClassLinker,
            // JIT Code cache fake collector.
            //JIT 代码缓存假收集器。
            kCollectorTypeJitCodeCache,
            // Hprof fake collector.
            //Hprof 假收藏家。
            kCollectorTypeHprof,
            // Fake collector for installing/removing a system-weak holder.
            //用于安装/移除系统弱支架的假收集器。
            kCollectorTypeAddRemoveSystemWeakHolder,
            // Fake collector type for GetObjectsAllocated
            //Get Object Allocated 的假收集器类型
            kCollectorTypeGetObjectsAllocated,
            // Fake collector type for ScopedGCCriticalSection
            //ScopedGCCriticalSection 的假收集器类型
            kCollectorTypeCriticalSection,
        };

        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_;

            //todo 作用域挂起所有实例的占位符
            // ScopedSuspendAll instance placeholder 作用域挂起所有实例的占位符
            std::unique_ptr<char[]> ssa_instance_;
            //todo 作用域GC临界段
            // ScopedGCCriticalSection instance placeholder
            std::unique_ptr<char[]> sgc_instance_;

            /**
             * 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的
             */
             //ScopedSuspendAll构造函数
            // art::ScopedSuspendAll::ScopedSuspendAll()
            void (*ssa_constructor_fnc_)(void *handle, const char *cause,
                                         bool long_suspend);

            //ScopedSuspendAll析构函数
            // todo art::ScopedSuspendAll::~ScopedSuspendAll()
            void (*ssa_destructor_fnc_)(void *handle);

            //todo ScopedGCCriticalSection 构造函数 Critical Section临界截面
            // art::gc::ScopedGCCriticalSection::ScopedGCCriticalSection()
            void (*sgc_constructor_fnc_)(void *handle, void *self, GcCause cause,
                                         CollectorType collector_type);

            //todo ScopedGCCriticalSection析构函数,Scoped作用域,范围,Critical Section临界截面,临界区;临界段;临界区域;关键部分;关键区段
            // art::gc::ScopedGCCriticalSection::~ScopedGCCriticalSection()
            void (*sgc_destructor_fnc_)(void *handle);

            //todo 指针 mutator突变体
            // art::Locks::mutator_lock_
            void **mutator_lock_ptr_;

            //专有的锁方法
            //Mutex 互斥
            //todo art::ReaderWriterMutex::ExclusiveLock
            void (*exclusive_lock_fnc_)(void *, void *self);

            //专有的解锁方法
            //Mutex 互斥
            //todo art::ReaderWriterMutex::ExclusiveUnlock
            void (*exclusive_unlock_fnc_)(void *, void *self);
        };

    }  // namespace leak_monitor
}  // namespace kwai
#endif  // KOOM_HPROF_DUMP_H

2.hprof_dump.cpp


#include <bionic/tls.h>
#include <dlfcn.h>
#include <hprof_dump.h>
#include <kwai_linker/kwai_dlfcn.h>
#include <log/kcheck.h>
#include <sys/prctl.h>
#include <wait.h>

#include <memory>

#undef LOG_TAG
#define LOG_TAG "HprofDump"

using namespace kwai::linker;

namespace kwai {
    namespace leak_monitor {

        //获取实例
        HprofDump &HprofDump::GetInstance() {
            static HprofDump hprof_dump;
            return hprof_dump;
        }

        //构造函数
        HprofDump::HprofDump() : init_done_(false), android_api_(0) {
            //获取api版本
            android_api_ = android_get_device_api_level();
        }

        //初始化
        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__) {
                // Over size for device compatibility
                ssa_instance_ = std::make_unique<char[]>(64);
                sgc_instance_ = std::make_unique<char[]>(64);

                ssa_constructor_fnc_ = (void (*)(void *, const char *, bool)) DlFcn::dlsym(
                        handle, "_ZN3art16ScopedSuspendAllC1EPKcb");
                KFINISHV_FNC(ssa_constructor_fnc_, DlFcn::dlclose, handle)

                ssa_destructor_fnc_ =
                        (void (*)(void *)) DlFcn::dlsym(handle, "_ZN3art16ScopedSuspendAllD1Ev");
                KFINISHV_FNC(ssa_destructor_fnc_, DlFcn::dlclose, handle)

                sgc_constructor_fnc_ =
                        (void (*)(void *, void *, GcCause, CollectorType)) DlFcn::dlsym(
                                handle,
                                "_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_"
                                "7GcCauseENS0_13CollectorTypeE");
                KFINISHV_FNC(sgc_constructor_fnc_, DlFcn::dlclose, handle)

                sgc_destructor_fnc_ = (void (*)(void *)) DlFcn::dlsym(
                        handle, "_ZN3art2gc23ScopedGCCriticalSectionD1Ev");
                KFINISHV_FNC(sgc_destructor_fnc_, DlFcn::dlclose, handle)

                mutator_lock_ptr_ =
                        (void **) DlFcn::dlsym(handle, "_ZN3art5Locks13mutator_lock_E");
                KFINISHV_FNC(mutator_lock_ptr_, DlFcn::dlclose, handle)

                exclusive_lock_fnc_ = (void (*)(void *, void *)) DlFcn::dlsym(
                        handle, "_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE");
                KFINISHV_FNC(exclusive_lock_fnc_, DlFcn::dlclose, handle)

                exclusive_unlock_fnc_ = (void (*)(void *, void *)) DlFcn::dlsym(
                        handle, "_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE");
                KFINISHV_FNC(exclusive_unlock_fnc_, DlFcn::dlclose, handle)
            }
            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__) {
                void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
                sgc_constructor_fnc_((void *) sgc_instance_.get(), self, kGcCauseHprof,
                                     kCollectorTypeHprof);
                ssa_constructor_fnc_((void *) ssa_instance_.get(), LOG_TAG, true);
                // avoid deadlock with child process
                exclusive_unlock_fnc_(*mutator_lock_ptr_, self);
                sgc_destructor_fnc_((void *) sgc_instance_.get());
            }

            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;

            if (android_api_ < __ANDROID_API_R__) {
                resume_vm_fnc_();   //resume vm的方法
            }
            if (android_api_ == __ANDROID_API_R__) {
                void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
                exclusive_lock_fnc_(*mutator_lock_ptr_, self);
                ssa_destructor_fnc_((void *) ssa_instance_.get());
            }
            int status;
            for (;;) {
                if (waitpid(pid, &status, 0) != -1 || errno != EINTR) {
                    //WIFEXITED 函数作用 Returns true if the process exited normally.
                    //status为0正常退出,dump成功
                    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;
            }
        }

    }  // namespace leak_monitor
}  // namespace kwai

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值