KOOM 的内存监控,分为三大块,分别为 Java 、Native 和 Thread,此篇主要是对 Native 层的内存监控模块的探索。
- KOOM 源码解读 - java 监控
- KOOM 源码解读 - native 监控
- KOOM 源码解读 - thread 监控
2、源码分析
1、启动监控
从 demo 中的入口开始
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 初始化 LeakMonitor
initLeakMonitor();
findViewById(R.id.btn_start_monitor).setOnClickListener(
// 1.1 启动 Native 内存泄露监控
view -> LeakMonitor.INSTANCE.start()
);
findViewById(R.id.btn_trigger_leaks).setOnClickListener(
// 手动生成 内存泄露
view -> NativeLeakTest.triggerLeak(new Object())
);
findViewById(R.id.btn_check_leaks).setOnClickListener(
// 检测 Native 内存泄露
view -> LeakMonitor.INSTANCE.checkLeaks()
);
findViewById(R.id.btn_stop_monitor).setOnClickListener(
// 停止 Native 内存泄露监控
(view) -> LeakMonitor.INSTANCE.stop()
);
}
private void initLeakMonitor() {
LeakMonitorConfig config = new LeakMonitorConfig.Builder()
.setLoopInterval(50000) // Set polling interval, time unit: millisecond
.setMonitorThreshold(16) // Set the threshold of the monitored memory block, unit: byte
.setNativeHeapAllocatedThreshold(0) // Set the threshold of how much memory allocated by
// the native heap reaches to start monitoring, unit: byte
.setSelectedSoList(new String[0]) // Set the monitor specific libraries, such as monitoring libcore.so, just write 'libcore'
.setIgnoredSoList(new String[0]) // Set the libraries that you need to ignore monitoring
.setEnableLocalSymbolic(false) // Set enable local symbolic, this is helpful in debug
// mode. Not enable in release mode
.setLeakListener(leaks -> { // Set Leak Listener for receive Leak info
if (leaks.isEmpty()) {
return;
}
// leaks 不为空,则检测到有 Native 内存泄露
StringBuilder builder = new StringBuilder();
for (LeakRecord leak : leaks) {
builder.append(leak.toString());
}
Toast.makeText(this, builder.toString(), Toast.LENGTH_SHORT).show();
})
.build();
MonitorManager.addMonitorConfig(config);
}
启动的流程:
- 调用 LeakMonitor.INSTANCE.start() 开启监控,再到父类的 LoopMonitor.startLoop,然后最终会不断调用 LeakMonitor.call() 方法,实现每隔一段时间就检测一次。具体逻辑可以查看 KOOM 源码解读 - 开篇
- call() 方法调用 nativeGetLeakAllocs() 方法
- nativeGetLeakAllocs 会调用到 jni_lead_monitor.cpp 中的 GetLeakAllocs 方法
- GetLeakAllocs 中通过 LeakMonitor::GetInstance().GetLeakAllocs() 拿到 未被回收的内存对象
- 然后返回给 LeakMonitorConfig 中设置的 LeakListener
2、手动生成泄露对象
// NativeLeakTestActivity.java
NativeLeakTest.triggerLeak(new Object())
// native-leak-test.cpp
extern "C" JNIEXPORT jlong
Java_com_kwai_koom_demo_nativeleak_NativeLeakTest_triggerLeak(
JNIEnv *env,
jclass,
jobject unuse/* this */) {
auto leak_test = []() {
TestMallocLeak();// 通过 malloc 开辟内存
TestCallocLeak();// 通过 calloc 开辟内存
TestReallocLeak();// 通过 realloc 开辟内存
TestMemalignLeak();// 通过 memalign 开辟内存
TestNewLeak();// 通过 new std::string 创建 string
TestNewArrayLeak();// 通过 new std::string[size] 创建 string数组
TestContainerLeak();// 创建 std::vector<std::string *> 容器
};
// 模拟多线程环境
for (int i = 0; i < NR_TEST_THREAD; i++) {
std::thread test_thread(leak_test);
test_thread.detach();
}
return TestJavaRefNative();// 开辟一块内存,把引用返回给 java 层
}
需要说明一下,这里的 new std::string 创建 string,其实底层也是使用 malloc 开辟内存,也会走 hook 的 malloc 。
3、Native 内存泄露监控
3.1 hook native 内存开辟与释放使用的方法
在 LeakMonitor 的 startLoop 中,会执行 nativeInstallMonitor() 方法,然后通过 jni 调用 jni_leak_monitor.cpp 的 InstallMonitor 函数,最终调用 leak_monitor.cpp 的 Install 函数
bool LeakMonitor::Install(std::vector<std::string> *selected_list,
std::vector<std::string> *ignore_list) {
// ...
std::vector<const std::string> register_pattern = {"^/data/.*\.so$"};
std::vector<const std::string> ignore_pattern = {".*/libkoom-native.so$",
".*/libxhook_lib.so$"};
if (ignore_list != nullptr) {
for (std::string &item : *ignore_list) {
ignore_pattern.push_back(".*/" + item + ".so$");
}
}
if (selected_list != nullptr && !selected_list->empty()) {
// only hook the so in selected list
register_pattern.clear();
for (std::string &item : *selected_list) {
register_pattern.push_back("^/data/.*/" + item + ".so$");
}
}
// hook 开辟内存的各种方法以及释放内存的方法
std::vector<std::pair<const std::string, void *const>> hook_entries = {
std::make_pair("malloc", reinterpret_cast<void *>(WRAP(malloc))),
std::make_pair("realloc", reinterpret_cast<void *>(WRAP(realloc))),
std::make_pair("calloc", reinterpret_cast<void *>(WRAP(calloc))),
std::make_pair("memalign", reinterpret_cast<void *>(WRAP(memalign))),
std::make_pair("posix_memalign",
reinterpret_cast<void *>(WRAP(posix_memalign))),
std::make_pair("free", reinterpret_cast<void *>(WRAP(free)))};
if (HookHelper::HookMethods(register_pattern, ignore_pattern, hook_entries)) {
has_install_monitor_ = true;
return true;
}
HookHelper::UnHookMethods();
live_alloc_records_.Clear();
memory_analyzer_.reset(nullptr);
ALOGE("%s Fail", __FUNCTION__);
return false;
}
当这些被 hook 的函数执行时,会调用到 leak_monitor.cpp 下面的方法
HOOK(void, free, void *ptr) {
free(ptr);
if (ptr) {
LeakMonitor::GetInstance().UnregisterAlloc(
reinterpret_cast<uintptr_t>(ptr));
}
}
HOOK(void *, malloc, size_t size) {
auto result = malloc(size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
size);
CLEAR_MEMORY(result, size);
return result;
}
HOOK(void *, realloc, void *ptr, size_t size) {
auto result = realloc(ptr, size);
if (ptr != nullptr) {
LeakMonitor::GetInstance().UnregisterAlloc(
reinterpret_cast<uintptr_t>(ptr));
}
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
size);
return result;
}
HOOK(void *, calloc, size_t item_count, size_t item_size) {
auto result = calloc(item_count, item_size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
item_count * item_size);
return result;
}
HOOK(void *, memalign, size_t alignment, size_t byte_count) {
auto result = memalign(alignment, byte_count);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
byte_count);
CLEAR_MEMORY(result, byte_count);
return result;
}
HOOK(int, posix_memalign, void **memptr, size_t alignment, size_t size) {
auto result = posix_memalign(memptr, alignment, size);
LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(*memptr),
size);
CLEAR_MEMORY(*memptr, size);
return result;
}
开辟内存的函数,最终都会调用到 LeakMonitor::GetInstance().OnMonitor() ,这里会调用 RegisterAlloc 函数,用于保存当前开辟的内存信息。
// 用于保存存活的内存
ConcurrentHashMap<intptr_t, std::shared_ptr<AllocRecord>> live_alloc_records_;
ALWAYS_INLINE void LeakMonitor::OnMonitor(uintptr_t address, size_t size) {
// ...
RegisterAlloc(address, size);
}
ALWAYS_INLINE void LeakMonitor::RegisterAlloc(uintptr_t address, size_t size) {
if (!address || !size) {
return;
}
auto unwind_backtrace = [](uintptr_t *frames, uint32_t *frame_count) {
*frame_count = StackTrace::FastUnwind(frames, kMaxBacktraceSize);
};
thread_local ThreadInfo thread_info;
auto alloc_record = std::make_shared<AllocRecord>();
alloc_record->address = CONFUSE(address);
alloc_record->size = size;
alloc_record->index = alloc_index_++;
memcpy(alloc_record->thread_name, thread_info.name, kMaxThreadNameLen);
unwind_backtrace(alloc_record->backtrace, &(alloc_record->num_backtraces));
// 保存到 live_alloc_records_
live_alloc_records_.Put(CONFUSE(address), std::move(alloc_record));
}
而 free 函数调用时,会调用 UnregisterAlloc 函数,把该内存从 live_alloc_records_ 中移除
ALWAYS_INLINE void LeakMonitor::UnregisterAlloc(uintptr_t address) {
live_alloc_records_.Erase(address);
}
3.2 native 内存泄露检测
在 LeakMonitor.kt 中,call() 方法调用 nativeGetLeakAllocs 方法,最终通过 jni 调用到 jni_leak_monitor.cpp 的 GetLeakAllocs() 函数
static void GetLeakAllocs(JNIEnv *env, jclass, jobject leak_record_map) {
ScopedLocalRef<jclass> map_class(env, env->GetObjectClass(leak_record_map));
jmethodID put_method;
GET_METHOD_ID(put_method, map_class.get(), "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
// 3.2.1 拿到泄露的内存数据
std::vector<std::shared_ptr<AllocRecord>> leak_allocs =
LeakMonitor::GetInstance().GetLeakAllocs();
for (auto &leak_alloc : leak_allocs) {
// ... 省略了一些数据判断与数据转换,还有栈回溯相关的信息
// 这里把转换后的数据,调用 java 中 map 的 put 方法,把数据保存到上层
ScopedLocalRef<jobject> no_use(
env,
env->CallObjectMethod(leak_record_map, put_method, memory_address.get(),
leak_record_ref.get()));
}
}
3.2.1 LeakMonitor::GetInstance().GetLeakAllocs() 拿到泄露的内存数据
std::vector<std::shared_ptr<AllocRecord>> LeakMonitor::GetLeakAllocs() {
KCHECK(has_install_monitor_);
// 3.2.2 拿到当前进程中不可访问的内存块
auto unreachable_allocs = memory_analyzer_->CollectUnreachableMem();
std::vector<std::shared_ptr<AllocRecord>> live_allocs;
std::vector<std::shared_ptr<AllocRecord>> leak_allocs;
// Collect live memory blocks
auto collect_func = [&](std::shared_ptr<AllocRecord> &alloc_info) -> void {
live_allocs.push_back(alloc_info);
};
// 拿到当前存活内存块,保存到 live_allocs
live_alloc_records_.Dump(collect_func);
// 内存泄露的判断标准
auto is_leak = [&](decltype(unreachable_allocs)::value_type &unreachable,
std::shared_ptr<AllocRecord> &live) -> bool {
auto live_start = CONFUSE(live->address);
auto live_end = live_start + live->size;
auto unreachable_start = unreachable.first;
auto unreachable_end = unreachable_start + unreachable.second;
// 当开始地址相等 或者 开始地址和结束地址在 unreachable 地址范围内,则判断为内存泄露块
return live_start == unreachable_start ||
live_start >= unreachable_start && live_end <= unreachable_end;
};
// Check leak allocation (unreachable && not free)
for (auto &live : live_allocs) {
for (auto &unreachable : unreachable_allocs) {
if (is_leak(unreachable, live)) {
// 保存泄露的内存块
leak_allocs.push_back(live);
// 把该内存块从 live_alloc_records_ 中移除
UnregisterAlloc(live->address);
}
}
}
return leak_allocs;
}
3.2.2 拿到当前进程中不可访问的内存块
static const char *kLibMemUnreachableName = "libmemunreachable.so";
// Just need the symbol in arm64-v8a so
// API level > Android O
static const char *kGetUnreachableMemoryStringSymbolAboveO =
"_ZN7android26GetUnreachableMemoryStringEbm";
// API level <= Android O
static const char *kGetUnreachableMemoryStringSymbolBelowO =
"_Z26GetUnreachableMemoryStringbm";
MemoryAnalyzer::MemoryAnalyzer()
: get_unreachable_fn_(nullptr), handle_(nullptr) {
auto handle = kwai::linker::DlFcn::dlopen(kLibMemUnreachableName, RTLD_NOW);
if (android_get_device_api_level() > __ANDROID_API_O__) {
get_unreachable_fn_ =
reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
handle, kGetUnreachableMemoryStringSymbolAboveO));
} else {
get_unreachable_fn_ =
reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
handle, kGetUnreachableMemoryStringSymbolBelowO));
}
}
MemoryAnalyzer::CollectUnreachableMem() {
std::vector<std::pair<uintptr_t, size_t>> unreachable_mem;
// 调用 libmemunreachable 的 GetUnreachableMemoryString 方法来获取任何不可访问的内存块
std::string unreachable_memory = get_unreachable_fn_(false, 1024);
// Unset "dumpable" for security
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
std::regex filter_regex("[0-9]+ bytes unreachable at [A-Za-z0-9]+");
std::sregex_iterator unreachable_begin(
unreachable_memory.begin(), unreachable_memory.end(), filter_regex);
std::sregex_iterator unreachable_end;
for (; unreachable_begin != unreachable_end; ++unreachable_begin) {
std::string line = unreachable_begin->str();
auto address =
std::stoul(line.substr(line.find_last_of(' ') + 1,
line.length() - line.find_last_of(' ') - 1),
0, 16);
auto size = std::stoul(line.substr(0, line.find_first_of(' ')));
unreachable_mem.push_back(std::pair<uintptr_t, size_t>(address, size));
}
return std::move(unreachable_mem);
}
可以看到,这里是通过 调用 libmemunreachable 的 GetUnreachableMemoryString 方法来获取任何不可访问的内存块。
Android 的 libmemunreachable 是一个零开销的本地内存泄漏检测器。 它会使用不精确的“标记-清除”垃圾回收器遍历所有本机内存,同时将任何不可访问的块报告为泄漏。
3.3 小结
- live_alloc_records_ 中保存有存活的内存块,即还未调用 free 函数释放内存的内存块;
- 通过调用 libmemunreachable 的 GetUnreachableMemoryString 方法来获取任何不可访问的内存块,存放到 unreachable_allocs ;
- 从 live_alloc_records_ 拿到存活的内存块,存放到 live_allocs ;
- 双重遍历 live_allocs 与 unreachable_allocs ,找到地址匹配的内存块,视为泄露的内存块,保存到 leak_allocs 。同时把该内存块从 live_alloc_records_ 中移除;
- 遍历结束后,把 leak_allocs 返回给上层处理;
3、总结
3.1 这里总结下主要的流程:
- 初始化时,hook 内存创建和内存释放的函数,把调用内存创建函数的内存块存放到 live_alloc_records_ ,调用 free 后再移除;
- 启动 LeakMonitor,监控 native 内存泄露;
- 每隔一段时间,通过 libmemunreachable 拿到不可访问的内存块,与 live_alloc_records_ 中的内存块对比,找出内存泄露的内存块;
- 每个泄露的内存块保存信息如栈回溯信息等,返回给上层处理;
3.2 主要用到的技术:
- hook 技术,使用的是 xhook 三方库;
- 使用 libmemunreachable 库,用于获取 不可访问的内存块 集合;