1. 分析系统问题的思路
- 从以下几方面入手进行分析:
- 内存
- 虚拟内存不足
- 物理内存不足
- 线程
- 句柄
- 内存
1.1 老化测试需关注的数据
# 说明:以下命令均需在root用户下执行,xx:为关注的进程名,pid: 为关注进程的ID
# 物理内存是否泄漏
procrank | grep xx # 重点关注USS<独占物理内存>大小
cat /proc/meminfo | grep MemAvailable
# 线程数量是否在不停增加
ps -t pid | wc -l
# 句柄数量是否在不停增加
ls -al /proc/pid/fd | wc -l
# 虚拟内存是否泄漏
cat /proc/pid/smaps | grep "anon:thread stack" | wc -l
cat /proc/pid/smaps | grep "anon:thread signal stack" | wc -l
cat /proc/pid/status | grep VmPeak # 关注VmPeak是否不断增长
2. 分析内存
- 目标:分析内存泄漏问题
2.1 进程内存使用总量
$ procrank | grep proc_name
- procrank命令输出说明:
- VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存
- RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
- PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
- USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
- 定位内存泄漏的方法:
- USS 的大小代表了只属于本进程正在使用的内存大小,这些内存在此进程被杀掉之后,会被完整的回收掉。
- USS 是针对某个进程开始有可疑内存泄露的情况,进行检测的最佳数字。
2.2 进程内存使用细节
$ dumpsys meminfo #查看所有的
$ dumpsys meminfo mediaserver #查看指定进程的
2.3 进程内存分布细节
$ cat /proc/pid/maps
$ cat /proc/pid/smaps | grep "anon:thread signal stack" | wc -l # java thread保护page数量
$ cat /proc/pid/smaps | grep "anon:thread stack" | wc -l # native thread保护page数量
3. 分析线程
3.1 查看进程所有线程(含name)
$ ps -t pid | wc -l
- 为方便分析问题,所有线程必须有Name
- 分析线程泄漏时,除了关注个数变化之外,还必须关注线程TID的变化
4. 分析文件句柄
- 目标:分析文件句柄泄漏问题
4.1 查看系统中所有文件句柄数:
$ lsof | busybox wc -l
#或
$ ls -al /proc/pid/fd | wc -l
4.2 查看指定进程的文件句柄数
$ cd /proc/pid/fd
$ ls | busybox wc -l
# 或
$ lsof -p $pid | busybox wc -l
# 查看进程句柄详细信息
ls -al /proc/<pid>/fd | wc -l
# 按时间排序
ls -la /proc/6146/fd | sort -t " " -k 6 # 顺序
ls -la /proc/6146/fd | sort -t " " -k 6 -r # 逆序
# -t 指定文本分隔符
# -k 指定排序列
# -n 按数字进行排序
# -r 翻转排序结果
4.3 查看系统最大文件限制
$ cat /proc/sys/fs/file-max
4.4 OOM (Out Of Memory)基础知识
-
虚拟内存管理
-
虚拟空间的管理是以进程为基础的,每个进程都有各自的虚拟空间
-
虚拟内存中虚拟地址/逻辑地址是连续的,便于灵活分配;虚拟内存可以是计算机呈现出比实际内存大的多的内存
-
每个进程的“内核空间”是为所有的进程所共享的
-
32位进程地址空间(0~4GB),Linux进程地址空间(虚拟内存)的组成:
-
一个进程的虚拟地址空间主要由两个数据结构来描述: mm_struct 和vm_area_struct
-
-
Linux 在分配内存时, 为了节省内存, 按需分配, 使用了延时分配以及Copy-On-Write 的策略.
- 延时分配即针对user space 申请memory 时, 先只是明面上的分配虚拟空间, 等到真正操作memory 时, 才真正分配具体的物理内存, 这个需要借助MMU 的data abort 转换成page fault 来达成. 这样就可以极大的避免因user space 过度申请memory, 或者错误申请memory 造成的memory 浪费.
- 而Copy-On-Write 即是在进程fork 时, 子进程和父进程使用同一份memory, 只有当某块memory 被更新时, 才重新copy 出新的一份. 这个在android 上表现也非常显著, 上层app 包括system server 都由zygote fork 出来, 并且没重新exec 新的bin, ART VM/Lib 的memory 都是共享的, 可以极大的节省Memory 的使用.
-
内存的整体使用情况
- 要分析memory leaks, 你需要知道总体的内存使用情况和划分. 以判断内存泄露是发生在user space, kernel space, mulit-media 等使用的memory, 从而进一步去判断具体的memory leaks.
- user space 使用的memory 即通常包括从进程直接申请的memory, 比如 malloc: 先mmap/sbrk 整体申请大块Memory 后再malloc 细分使用, 比如stack memory, 直接通过mmap 从系统申请; 以及因user space 进程打开文件所使用的page cache, 以及使用ZRAM 压缩 user space memory 存储所占用的memory
- kernel space 使用的memory 通常包括 kernel stack, slub, page table, vmalloc, shmem 等.
- mulit-media 使用的memory 通常使用的方式包括 ion, gpu 等.
- 其他方式的memory 使用, 此类一般直接从buddy system 中申请出以page 为单位的memory, android 中比较常见如ashmem.
- 而从进程的角度来讲, 通常情况下进程所使用的memory, 都会通过mmap 映射到进程空间后访问使用(注: 也会一些非常特别异常的流程, 没有mmap 到进程空间), 所以进程的memory maps 资讯是至关重要的.
-
Android中导致OOM的主要原因:
4.4.1 堆内存分配失败
// 系统源码文件:/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
//抛出时的错误信息:
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
// 调用/art/runtime/thread.cc中如下函数抛出异常
void Thread::ThrowOutOfMemoryError(const char* msg) // 参数msg中有OOM时的错误信息
- 这是在进行堆内存分配时抛出的OOM错误,这里也可以细分成两种不同的类型:
- 为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。
- 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:failed due to fragmentation (required continguous free “<< required_bytes << “ bytes for a new buffer where largest contiguous free ” << largest_continuous_free_pages << “ bytes)”; 其详细代码在art/runtime/gc/allocator/rosalloc.cc中
4.4.2 创建线程失败
//系统源码文件:/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)
//抛出时的错误信息:
// "Could not allocate JNI Env"
// 或者
// StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
- 这是创建线程时抛出的OOM错误,且有多种错误信息。下面是根据源码整理的Android中创建线程的步骤,其中两个关键节点是创建JNIEnv结构体和创建线程,而这两步均有可能抛出OOM
- 创建线程也可以归纳为两个步骤:
- 调用mmap分配栈内存。这里mmap flag中指定了MAP_ANONYMOUS,即匿名内存映射。这是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候触发内核的缺页中断,然后中断处理函数再分配物理内存
- 调用clone方法进行线程创建
- 分配栈内存失败是由于进程的虚拟内存不足,抛出错误信息如下:
W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize 4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:753)
- clone方法失败是因为线程数超出了限制,抛出错误信息如下:
W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1078)
4.4.3 创建JNIEnv失败
- JNI创建步骤:
- 通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存。
- 再通过Linux的mmap调用映射到用户态虚拟内存地址空间
- 创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败,抛出错误信息如下:
E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:730)
- 调用mmap时,如果进程虚拟内存地址空间耗尽,也会导致创建JNIEnv失败,抛出错误信息如下:
E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)
4.5 OOM (Out Of Memory)实例分析
- mmap failed: Out of Memory
- mmap只负责分配虚拟内存,出现OOM的原因是虚拟空间用完,即虚拟地址泄漏
- 线程已退出,但其在smaps中的保护page没有释放,即
$ ps -t pid | wc -l # 数量不变
$ cat /proc/pid/smaps | grep "anon:thread stack" | wc -l # 数量不断增加
- 本质原因:
- pthread_create()创建的线程默认是joinable的,joinable线程没有被其他线程joined就会造成内存泄漏
- 对于joinable线程,系统会分配私有内存存储线程结束状态、线程栈、线程ID等等资源。这些资源会一直存在,直到线程结束并且线程被其他线程joined
- 确保joinable线程资源得到释放的两个条件是:线程退出、被其他线程joined
- 对于detached线程,如果其退出,那么系统会自动回收其占用的资源
- joinable线程没有被其他线程joined造成内存泄漏的验证
- 有虚拟地址空间泄漏
#include<stdio.h>
#include<pthread.h>
void run() {
pthread_exit(0);
}
int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
usleep(10);
count++;
}
return 0;
}
- continue
- 无虚拟地址空间泄漏
#include<stdio.h>
#include<pthread.h>
void run() {
pthread_exit(0);
}
int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
pthread_join(thread, NULL); // 将释放stack和anon:thread stack guard page
usleep(10);
count++;
}
return 0;
}
5. JNI 参考表溢出
5.1 全局参考表溢出
- 相关错误信息
JNI ERROR (app bug): global reference table overflow (max=512000)
- 它是一个进程的JNI参考表
- 从以下两方便定位此问题:
- 在JNI中引用之后,没有正常释放(此类问题一看代码便知)
- 由线程泄漏引起,此类问题会非常快速地引起溢出
5.2 局部参考表溢出
- 相关错误信息
JNI ERROR (app bug): local reference table overflow (max=512)
- 它是一个线程的JNI参考表
- 解决方法:此类问题是由于没有正常释放引用导致的
6. 待机问题
6.1 应用程序持有wakelock
$ dumpsys power
$ dumpsys power | busybox grep "Wake Locks" # 查看多少个应用持有WakeLock
- 查看Wake Locks: size的值, 若其值不为0,则不能进入深度待机,然后分析哪些应用持有wake_lock
6.2 设备驱动持有wakelock
- 查看内核持有的wake_lock状态
$ cat /sys/power/wake_lock
- 在待机过程中被wakeup_sources唤醒
$ cat /sys/kernel/debug/wakeup_sources
- 在linux-3.10上,wake_lock的本质就是wakeup_sources
- 查看active_since的值是否为非0,若为非0,表明此wakeup_source已经阻止系统进入休眠多少ms
6.3 待机唤醒Kernel入口 state_store
- 文件名:linux-3.10\kernel\power\main.c
static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t n)
{
suspend_state_t state;
int error;
// added by luohj for debug on 2019.8.17
printk(KERN_ERR "***%s:%d, buf=%s\n", __FUNCTION__, __LINE__, buf);
error = pm_autosleep_lock();
if (error)
return error;
if (pm_autosleep_state() > PM_SUSPEND_ON) {
error = -EBUSY;
goto out;
}
state = decode_state(buf, n);
if (state < PM_SUSPEND_MAX)
error = pm_suspend(state);
else if (state == PM_SUSPEND_MAX)
error = hibernate();
else
error = -EINVAL;
out:
pm_autosleep_unlock();
return error ? error : n;
}
7. 查看所有USB设备
$ cat /sys/kernel/debug/usb/devices // 可看到其驱动名
8. 禁止鼠标唤醒
- 文件名: frameworks\base\policy\src\com\android\internal\policy\impl\PhoneWindowManager.java
public int interceptMotionBeforeQueueingWhenScreenOff(int policyFlags) {
int result = 0;
final boolean isWakeMotion = (policyFlags
& (WindowManagerPolicy.FLAG_WAKE | WindowManagerPolicy.FLAG_WAKE_DROPPED)) != 0;
if (isWakeMotion) {
result |= ACTION_WAKE_UP;
}
//return result;
return 0; // modified by luohj to prohibit mouse wakeup the system 2019.8.17
}