赞
踩
本系列将围绕下面几个方面来介绍内存监控方案:
FD 数量
线程数量
java 堆
Native 内存
FD 即 File Descriptor (文件描述符),对于 Android 来说,一个进程能使用的 FD 资源是有限的,在 Android9 前,最多限制 1024,Android9 及以上,最多 3w 余个。而 FD 达到上限后,没资源了就会产生各种问题,跟 OOM 一样,很难被定位到,因为 crash 后的堆栈信息可能并没指向“始作俑者”。所以 FD 泄漏的监控是很有必要的。
那什么操作会占用 FD 资源呢?常见的:文件读写、Socket 通信、创建 java 线程、启用 HandlerThread、创建 Window 、数据库操作等。
以创建 java 线程为例,创建线程首先会在 Native 层创建 JNIEnv,这步包括:
其中在创建匿名共享内存时,会打开 /dev/ashmem 文件,所以创建线程需要一个 FD。
我们通过读取 /proc 下的虚拟文件来获取进程的 FD, 代码可见matrix,具体方法见下:
读取进程文件 /proc/pid/fd, 计算文件数量。
遍历进程文件 /proc/pid/fd,并通过 readlink 解释文件链接。(在 RequiresApi 21 及以上可以直接使用系统方法 Os.readlink(file.absolutePath))
不清楚怎么调用 c++ 代码的,可以看下我之前博客,Android修炼系列(十九),来编译一个自己的 so 库吧,在我的三星S8测试机上部分数据如下:
方案:直接开个线程,每 10s 周期检查一次当前进程 FD 数量,当 FD 数量达到阈值时(如90%),就抓取一次当前进程的 FD 信息、线程信息、内存快照信息。
我们可以拿 FD 信息内的路径,用来定位 IO 问题,通过线程名称,来定位 java 线程和 HandlerThread 的问题,通过内存快照来排查 Socket 和 window 等问题。
关于如何 dumpHprofData 内存快照,后面会单独写一节。
每个线程都对应着一个栈内存,在 Android 中,一个 java 线程大概占用 1M 栈内存,如果是 native 线程,可以通过 pthread_atta_t 来指定栈大小,如果不加限制的创建线程,就会导致 OOM crash。
系统从下面 3 个方面限制了线程的数量:
配置文件 /proc/sys/kernel/threads-max 指定了系统范围内的最大线程数量。
Linux resource limits 的 RLIMIT_NPROC 参数对应了应用的最大线程数量。
虚拟地址空间不足或内核分配 vma 失败等内存原因,也限制了能创建的线程数量。
试了下直接读取 threads-max 文件,没有权限诶。
我们可以通过 ThreadGroup 来获取所有 java 线程:
- val threadGroup: ThreadGroup = Looper.getMainLooper().thread.threadGroup
- val threadList = arrayOfNulls<Thread>(threadGroup.activeCount() * 2)
- val size = threadGroup.enumerate(threadList);
- 复制代码
native 的线程数量,我们可以读取 /proc/[ pid ]/status 中的 Threads 字段的值,其中 /proc/[ pid ]/task 目录下记录着所有线程的 tid、线程名等信息:
- File(String.format("/proc/%s/status", Process.myPid())).forEachLine { line ->
- when {
- line.startsWith("Threads") -> {
- Log.d("mjzuo", line)
- }
- }
- }
- 复制代码
方案:关于监控线程数量的监控,与 FD 的思想一样,都是开一个子线程,周期检查应用的当前线程数,当超过阈值时,抓取线程信息并上报。
不管java 线程,还是 native 线程都是通过 pthread_create 方法创建的。常见的还有 pthread_detach、pthread_join、pthread_exit API,当我们通过 pthread_create 来创建线程时,线程状态默认都是 joinable 状态的,只有 detach 状态的线程,才能在线程执行完退出时自动释放栈内存,否则就需要等待调用 join 来释放内存。
即 create 线程后,不调用 detach 或 join 就直接 exit 退出,栈内存不会释放,会造成线程泄漏。
既然知道技术原理了,那么监控手段就呼之欲出了,hook 上面几个接口,记录 joinable 状态的泄漏线程信息。 以 KOOM源码为例:
java 层的代码就不说了,直接看下 c++ 的逻辑吧,这是桥梁 JNI 接口:
- JNIEXPORT void JNICALL
- Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_start(
- JNIEnv *env, jclass obj) {
- koom::Log::info("koom-thread", "start");
- koom::Start();
- }
-
- JNIEXPORT void JNICALL
- Java_com_kwai_performance_overhead_thread_monitor_NativeHandler_stop(
- JNIEnv *env, jclass obj) {
- koom::Stop();
- }
- 复制代码
我们来看下 koom.cpp#Start 接口:
- void Start() {
- if (isRunning) {
- return;
- }
- // 初始化数据
- delete sHookLooper;
- sHookLooper = new HookLooper(); // 创建 HookLooper 用来转发消息
- koom::ThreadHooker::Start(); // 开始 hook
- isRunning = true;
- }
- 复制代码
这是 thread_hook.cpp#Start 接口,其中dlopencb.h 的逻辑就不贴了,目录在 koom-common/third-party/xhook/src/main/cpp/xhook/src/ :
- void ThreadHooker::Start() { ThreadHooker::InitHook(); }
-
- void ThreadHooker::InitHook() {
- koom::Log::info(thread_tag, "HookSo init hook");
- std::set<std::string> libs;
- DlopenCb::GetInstance().GetLoadedLibs(libs); // 拿到要被hook的动态库
- HookLibs(libs, Constant::kDlopenSourceInit); // hook
- DlopenCb::GetInstance().AddCallback(DlopenCallback); // 监听,其中GetLoadedLibs(libs, true) 才会回调
- }
- 复制代码
这是 thread_hook.cpp#HookLibs 接口
- void ThreadHooker::HookLibs(std::set<std::string> &libs, int source) {
- koom::Log::info(thread_tag, "HookSo lib size %d", libs.size());
- if (libs.empty()) {
- return;
- }
- bool hooked = false;
- pthread_mutex_lock(&DlopenCb::hook_mutex);
- xhook_clear(); // 清除 xhook 的缓存,重置所有的全局标示
- for (const auto &lib : libs) {
- hooked |= ThreadHooker::RegisterSo(lib, source); // 开始 hook so 方法
- }
- if (hooked) {
- int result = xhook_refresh(0); // 0:表示执行同步的 hook 操作,1:表示执行异步的 hook 操作
- koom::Log::info(thread_tag, "HookSo lib Refresh result %d", result);
- }
- pthread_mutex_unlock(&DlopenCb::hook_mutex);
- }
- 复制代码
这是我们要hook 的方法:thread_hook.cpp#RegisterSo
- bool ThreadHooker::RegisterSo(const std::string &lib, int source) {
- if (IsLibIgnored(lib)) { // 过滤不hook的库,不贴了
- return false;
- }
- auto lib_ctr = lib.c_str();
- koom::Log::info(thread_tag, "HookSo %d %s", source, lib_ctr);
- xhook_register(lib_ctr, "pthread_create",
- reinterpret_cast<void *>(HookThreadCreate), nullptr);
- xhook_register(lib_ctr, "pthread_detach",
- reinterpret_cast<void *>(HookThreadDetach), nullptr);
- xhook_register(lib_ctr, "pthread_join",
- reinterpret_cast<void *>(HookThreadJoin), nullptr);
- xhook_register(lib_ctr, "pthread_exit",
- reinterpret_cast<void *>(HookThreadExit), nullptr);
-
- return true;
- }
- 复制代码
当调用 pthread_create 方法时,会被拦截进我们hook的方法:
- int ThreadHooker::HookThreadCreate(pthread_t *tidp, const pthread_attr_t *attr,
- void *(*start_rtn)(void *), void *arg) {
- if (hookEnabled() && start_rtn != nullptr) {
- ... // hook 返回的信息
- if (thread != nullptr) {
- koom::CallStack::JavaStackTrace(thread, hook_arg->thread_create_arg->java_stack); // java栈
- }
- koom::CallStack::FastUnwind(thread_create_arg->pc, koom::Constant::kMaxCallStackDepth); // native 栈回溯
- thread_create_arg->stack_time = Util::CurrentTimeNs() - time;
- return pthread_create(tidp, attr,
- reinterpret_cast<void *(*)(void *)>(HookThreadStart),
- reinterpret_cast<void *>(hook_arg));
- }
- return pthread_create(tidp, attr, start_rtn, arg);
- }
- 复制代码
随后调用 thread_hook.cpp#HookThreadStart
- ALWAYS_INLINE void ThreadHooker::HookThreadStart(void *arg) {
- ... // 拿hook信息,组HookAddInfo,具体不贴了
- auto info = new HookAddInfo(tid, Util::CurrentTimeNs(), self,
- state == PTHREAD_CREATE_DETACHED,
- hookArg->thread_create_arg);
-
- sHookLooper->post(ACTION_ADD_THREAD, info); // 转发 HookLooper.cpp#handle
- void *(*start_rtn)(void *) = hookArg->start_rtn;
- void *routine_arg = hookArg->arg;
- delete hookArg;
- start_rtn(routine_arg);
- }
- 复制代码
消息被转发到 HookLooper.cpp#handle:
- case ACTION_ADD_THREAD: {
- koom::Log::info(looper_tag, "AddThread");
- auto info = static_cast<HookAddInfo *>(data);
- holder->AddThread(info->tid, info->pthread, info->is_thread_detached,
- info->time, info->create_arg); // 再转发
- delete info;
- break;
- }
- 复制代码
消息被转发到 thread_holder.cpp#AddThread,在这里就记录了线程,并标记状态:
- void ThreadHolder::AddThread(int tid, pthread_t threadId, bool isThreadDetached,
- int64_t start_time, ThreadCreateArg *create_arg) {
- bool valid = threadMap.count(threadId) > 0;
- if (valid) return;
-
- koom::Log::info(holder_tag, "AddThread tid:%d pthread_t:%p", tid, threadId);
- auto &item = threadMap[threadId]; // 线程列表
- item.Clear();
- item.thread_internal_id = threadId;
- item.thread_detached = isThreadDetached; // 这个就是上面提到的线程状态,false
- item.startTime = start_time;
- item.create_time = create_arg->time;
- item.id = tid;
- ... // 栈内容就不贴了
- delete create_arg;
- koom::Log::info(holder_tag, "AddThread finish");
- }
- 复制代码
其他方法就不细说了,我们来看下当消息被转发过来时,detach 和 join 的逻辑是一样的,所以就贴一个了:
- void ThreadHolder::DetachThread(pthread_t threadId) {
- bool valid = threadMap.count(threadId) > 0;
- koom::Log::info(holder_tag, "DetachThread tid:%p", threadId);
- if (valid) {
- threadMap[threadId].thread_detached = true; // 将状态改变
- } else {
- leakThreadMap.erase(threadId); // 从泄漏线程列表中移除
- }
- }
- 复制代码
这是 exit 的逻辑,在这里将非 detached 状态的线程都加入到泄漏集合里,注意如果 exit 后面再调用 join 还是可以移除掉的:
- void ThreadHolder::ExitThread(pthread_t threadId, std::string &threadName,
- long long int time) {
- bool valid = threadMap.count(threadId) > 0;
- if (!valid) return;
- auto &item = threadMap[threadId];
- ...
- if (!item.thread_detached) {
- // 泄露了
- koom::Log::error(holder_tag,
- "Exited thread Leak! Not joined or detached!\n tid:%p",
- threadId);
- leakThreadMap[threadId] = item;
- }
- threadMap.erase(threadId); // 从线程集合移除
- koom::Log::info(holder_tag, "ExitThread finish");
- }
- 复制代码
受篇幅影响(os内心:累了,不想再爱了),虚拟内存、java堆、native 内存监控的内容会放在下节。
本节完。
参考:
cloud.tencent.com/developer/a…
作者:Zuo
链接:https://juejin.cn/post/7080461351474167844
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。