本系列将围绕下面几个方面来介绍内存监控方案:
-
FD 数量
-
线程数量
-
虚拟内存
-
java 堆
-
Native 内存
FD 监控
FD 即 File Descriptor (文件描述符),对于 Android 来说,一个进程能使用的 FD 资源是有限的,在 Android9 前,最多限制 1024,Android9 及以上,最多 3w 余个。而 FD 达到上限后,没资源了就会产生各种问题,跟 OOM 一样,很难被定位到,因为 crash 后的堆栈信息可能并没指向“始作俑者”。所以 FD 泄漏的监控是很有必要的。
那什么操作会占用 FD 资源呢?常见的:文件读写、Socket 通信、创建 java 线程、启用 HandlerThread、创建 Window 、数据库操作等。
以创建 java 线程为例,创建线程首先会在 Native 层创建 JNIEnv,这步包括:
- 通过匿名共享内存分配 4KB 的内核态内存。
- 通过 mmap 映射到用户态虚拟内存地址空间。
其中在创建匿名共享内存时,会打开 /dev/ashmem 文件,所以创建线程需要一个 FD。
FD 信息
我们通过读取 /proc 下的虚拟文件来获取进程的 FD, 代码可见matrix,具体方法见下:
- 读取进程状态 /proc/pid/limits, 并解释 limit.rlim_max 字段。(我实际测了下,rlim_cur 和 rlim_max 值一样)
- 读取进程文件 /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 内存监控的内容会放在下节。
本节完。
参考: