一.解决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;