0%

Android-OOM

OOM痛点

OOM(Out Of Memory)成为奔溃统计平台上的疑难杂症之一,大部分业务开发人员对于线上OOM问题一般都是暂不处理,一方面是因为OOM问题没有足够的log,无法在短期内分析解决,另一方面可能是忙于业务迭代、身心疲惫,没有精力去研究OOM的解决方案。

作者:蓝师傅
链接:https://juejin.cn/post/7074762489736478757
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

OOM问题分类

-OOM可以大致归为以下3类:

  • 线程数太多
  • 打开太多文件
  • 内存不足

线程数太多

查看系统对每个进程的线程数限制

cat /proc/sys/kernel/threads-max
不同设备的threads-max限制是不一样的,有些厂商的低端机型threads-max比较小,容易出现此类OOM问题

root手机查看

模拟器 部分手机需要root,如小米等硬核

查看当前进程运行的线程数

cat proc/{pid}/status
当线程数超过/proc/sys/kernel/threads-max中规定的上限时就会触发OOM。

真机 ulimit限制

查看当前总进程限制的线程数

ulimit -a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1|rubens:/ $ ulimit -a
-t: time(cpu-seconds) unlimited
-f: file(blocks) unlimited
-c: coredump(blocks) 0
-d: data(KiB) unlimited
-s: stack(KiB) 8192
-l: lockedmem(KiB) unlimited
-n: nofiles(descriptors) 32768
-p: processes 26248
-i: sigpending 26248
-q: msgqueue(bytes) 819200
-e: maxnice 40
-r: maxrtprio 0
-m: resident-set(KiB) unlimited
-v: address-space(KiB) unlimited
-x: filelocks unlimited

参数解析
max memory size - 最大内存限制,在64位系统上通常都设置成unlimited
max user processes - 每用户总的最大进程数(包括线程) 总的!!!
virtual memory - 虚拟内存限制,在64位系统上通常都设置成unlimited

既然系统对每个进程的线程数有限制,那么解决这个问题的关键就是尽可能降低线程数的峰值。

查看当前进程运行的线程数

cat proc/{Pid}/status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
rubens:/ $ cat proc/18425/status
Name: tudy.pingindemo
Umask: 0077
State: S (sleeping)
Tgid: 18425
Ngid: 0
Pid: 18425
PPid: 825
TracerPid: 0
Uid: 10375 10375 10375 10375
Gid: 10375 10375 10375 10375
FDSize: 256
Groups: 9997 20375 50375 99909997
VmPeak: 6912748 kB
VmSize: 6844832 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 176600 kB
VmRSS: 173880 kB
RssAnon: 48700 kB
RssFile: 123828 kB
RssShmem: 1352 kB
VmData: 1300552 kB
VmStk: 8192 kB
VmExe: 4 kB
VmLib: 179852 kB
VmPTE: 1252 kB
VmSwap: 25156 kB
CoreDumping: 0
THP_enabled: 1
Threads: 27
SigQ: 0/26248
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000080001204
SigIgn: 0000000000000001
SigCgt: 0000006e400084f8
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 2
Seccomp_filters: 1
Speculation_Store_Bypass: thread vulnerable
Cpus_allowed: ff
Cpus_allowed_list: 0-7
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 169
nonvoluntary_ctxt_switches: 123

参数解析

当前Threads: 27

命令-扩展

ps -ef看到的是进程列表
线程可以通过ps -eLf来查看

根据包名查看当前进程

adb shell ps | grep xxx

1
u0_a376      22592{PID}   825 6628656 169584 0  0 S com.study.pingindemo

得到当前进程pid或名字则查看当前所有的线程

adb shell ps -T | grep {PID}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
u0_a377      26204 26204   825 6804816 189004 0                   0 S tudy.pingindemo
u0_a377 26204 26227 825 6804816 189004 0 0 S Signal Catcher
u0_a377 26204 26228 825 6804816 189004 0 0 S perfetto_hprof_
u0_a377 26204 26229 825 6804816 189004 0 0 S ADB-JDWP Connec
u0_a377 26204 26230 825 6804816 189004 0 0 S Jit thread pool
u0_a377 26204 26231 825 6804816 189004 0 0 S HeapTaskDaemon
u0_a377 26204 26232 825 6804816 189004 0 0 S ReferenceQueueD
u0_a377 26204 26233 825 6804816 189004 0 0 S FinalizerDaemon
u0_a377 26204 26234 825 6804816 189004 0 0 S FinalizerWatchd
u0_a377 26204 26235 825 6804816 189004 0 0 S binder:26204_1
u0_a377 26204 26236 825 6804816 189004 0 0 S binder:26204_2
u0_a377 26204 26237 825 6804816 189004 0 0 S binder:26204_3
u0_a377 26204 26244 825 6804816 189004 0 0 S Profile Saver
u0_a377 26204 26247 825 6804816 189004 0 0 S RenderThread
u0_a377 26204 26262 825 6804816 189004 0 0 S Binder:intercep
u0_a377 26204 26263 825 6804816 189004 0 0 S Timer-0
u0_a377 26204 26264 825 6804816 189004 0 0 S Timer-1
u0_a377 26204 26265 825 6804816 189004 0 0 S FramePolicy
u0_a377 26204 26266 825 6804816 189004 0 0 S launch
u0_a377 26204 26271 825 6804816 189004 0 0 S mali-event-hand
u0_a377 26204 26272 825 6804816 189004 0 0 S mali-mem-purge
u0_a377 26204 26273 825 6804816 189004 0 0 S mali-cpu-comman
u0_a377 26204 26274 825 6804816 189004 0 0 S ged-swd
u0_a377 26204 26276 825 6804816 189004 0 0 S hwuiTask0
u0_a377 26204 26277 825 6804816 189004 0 0 S hwuiTask1
u0_a377 26204 26279 825 6804816 189004 0 0 S RenderThread
u0_a377 26204 26288 825 6804816 189004 0 0 S binder:26204_2
u0_a377 26204 26320 825 6804816 189004 0 0 S queued-work-loo
u0_a377 26204 26346 825 6804816 189004 0 0 S binder:26204_4
u0_a377 26204 26427 825 6804816 189004 0 0 S binder:26204_5
u0_a377 26204 26429 825 6804816 189004 0 0 S 26204-ScoutStat

java

在 Android 中,可以使用如下代码来获取当前线程数:

int threadCount = Thread.activeCount();
activeCount() 方法返回当前活动线程的数量,也就是当前线程数。

注意:activeCount() 方法只能获取到在 Java 虚拟机中创建的线程数,无法获取到其他线程(例如 C++ 创建的线程)的数量。

排查

我们并不清楚到底是哪个部分有问题导致的线程数的增长,所以我们需要一个每1s可以打印一下当前的线程数再通过页面交互来确定到底是哪里出现的问题,可以使用watch命令来完成我们的想法,如下所示:

watch -n 1 -d ‘adb shell ps -T | grep u0_a589 | wc -l’

watch -n 1 -d ‘adb shell ps -T | grep u0_a377 | wc -l’

操作APP时可以试试的看到线程数的大小,并且通过观察看到那类的线程名字在增多.

线程优化

禁用 new Thread
不过这种方式存在两个问题:

无法解决老代码的new Thread;
对于第三方库无法控制。

无侵入性的new Thread 优化

创建一个Thread的子类叫ShadowThread吧,重写start方法,调用自定义的线程池CustomThreadPool来执行任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ShadowThread extends Thread {

@Override
public synchronized void start() {
Log.i("ShadowThread", "start,name="+ getName());
CustomThreadPool.THREAD_POOL_EXECUTOR.execute(new MyRunnable(getName()));
}

class MyRunnable implements Runnable {

String name;
public MyRunnable(String name){
this.name = name;
}

@Override
public void run() {
try {
ShadowThread.this.run();
Log.d("ShadowThread","run name="+name);
} catch (Exception e) {
Log.w("ShadowThread","name="+name+",exception:"+ e.getMessage());
RuntimeException exception = new RuntimeException("threadName="+name+",exception:"+ e.getMessage());
exception.setStackTrace(e.getStackTrace());
throw exception;
}
}
}
}

在编译期,hook 所有new Thread字节码,全部替换成我们自定义的ShadowThread,这个难度应该不大,按部就班,
我们先确认new Thread和new ShadowThread对应字节码差异,可以安装一个ASM Bytecode Viewer插件,如下所示

由于将任务放到线程池去执行,假如线程奔溃了,我们不知道是哪个线程出问题,所以自定义ShadowThread中的内部类MyRunnable 的作用是:在线程出现异常的时候,将异常捕获,还原它的名字,重新抛出一个信息更全的异常。

分析线程词

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}

1.corePoolSize:核心线程数量。核心线程默认情况下即使空闲也不会释放,除非设置allowCoreThreadTimeOut为true。
2.maximumPoolSize:最大线程数量。任务数量超过核心线程数,就会将任务放到队列中,队列满了,就会启动非核心线程执行任务,线程数超过这个限制就会走拒绝策略;
3.keepAliveTime:空闲线程存活时间
4.unit:时间单位
5.workQueue:队列。任务数量超过核心线程数,就会将任务放到这个队列中,直到队列满,就开启新线程,执行队列第一个任务。
6.threadFactory:线程工厂。实现new Thread方法创建线程

线程泄露

1.主要监控native线程的几个生命周期方法:pthread_create、pthread_detach、pthread_join、pthread_exit

2.hook 以上几个方法,用于记录线程的生命周期和堆栈,名称等信息;

3.当发现一个joinable的线程在没有detach或者join的情况下,执行了pthread_exit,则记录下泄露线程信息;

4.在合适的时机,上报线程泄露信息

3 打开太多文件
Linux 系统一切皆文件,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面)

cd /proc/10654/fd

ls