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