KOOM 源码解读 - native 内存监控

3,401 阅读5分钟

KOOM 的内存监控,分为三大块,分别为 JavaNativeThread,此篇主要是对 Native 层的内存监控模块的探索。

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);
  }

启动的流程:

  1. 调用 LeakMonitor.INSTANCE.start() 开启监控,再到父类的 LoopMonitor.startLoop,然后最终会不断调用 LeakMonitor.call() 方法,实现每隔一段时间就检测一次。具体逻辑可以查看 KOOM 源码解读 - 开篇
  2. call() 方法调用 nativeGetLeakAllocs() 方法
  3. nativeGetLeakAllocs 会调用到 jni_lead_monitor.cpp 中的 GetLeakAllocs 方法
  4. GetLeakAllocs 中通过 LeakMonitor::GetInstance().GetLeakAllocs() 拿到 未被回收的内存对象
  5. 然后返回给 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 小结

  1. live_alloc_records_ 中保存有存活的内存块,即还未调用 free 函数释放内存的内存块;
  2. 通过调用 libmemunreachable 的 GetUnreachableMemoryString 方法来获取任何不可访问的内存块,存放到 unreachable_allocs ;
  3. 从 live_alloc_records_ 拿到存活的内存块,存放到 live_allocs ;
  4. 双重遍历 live_allocs 与 unreachable_allocs ,找到地址匹配的内存块,视为泄露的内存块,保存到 leak_allocs 。同时把该内存块从 live_alloc_records_ 中移除;
  5. 遍历结束后,把 leak_allocs 返回给上层处理;

3、总结

3.1 这里总结下主要的流程:

  1. 初始化时,hook 内存创建和内存释放的函数,把调用内存创建函数的内存块存放到 live_alloc_records_ ,调用 free 后再移除;
  2. 启动 LeakMonitor,监控 native 内存泄露;
  3. 每隔一段时间,通过 libmemunreachable 拿到不可访问的内存块,与 live_alloc_records_ 中的内存块对比,找出内存泄露的内存块;
  4. 每个泄露的内存块保存信息如栈回溯信息等,返回给上层处理;

3.2 主要用到的技术:

  1. hook 技术,使用的是 xhook 三方库;
  2. 使用 libmemunreachable 库,用于获取 不可访问的内存块 集合;

4、参考

  1. https://github.com/KwaiAppTeam/KOOM
  2. https://source.android.google.cn/docs/core/tests/debug/native-memory?hl=zh_cn