解决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