由一个stack OOM引发的血案

近期在App监测平台上发现如下错误信息:

java.lang.OutOfMemoryError: pthread_create (stack size 16384 bytes) failed: Try again
	at java.lang.VMThread.create(VMThread.java)
	at java.lang.Thread.start(Thread.java:1029)
	at com.migu.uem.statistics.page.b.e(b.java)
	at com.migu.uem.statistics.page.b.a(b.java)
...
java.lang.OutOfMemoryError: pthread_create (stack size 16384 bytes) failed: Try again(Heap Size=15488KB, Allocated=12634KB)
	at java.lang.VMThread.create(VMThread.java)
	at java.lang.Thread.start(Thread.java:1063)
...
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
	at java.lang.Thread.nativeCreate(Thread.java)
	at java.lang.Thread.start(Thread.java:1076)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:920)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1338)
...

内存溢出(OOM)了?是啊,明明白白写着“java.lang.OutOfMemoryError”。然而,有没有注意到错误信息里都有关于stack字样?对,这是由于栈内存不足造成的,而不是常见的堆内存溢出。程序猿们经常上的网站StackOverFlow终于出现在程序里了!其实,准确地说,此时并没有发生栈溢出,而是连栈都没有分配成功 :P 从调用栈上可以发现,都是在本地方法创建线程的时候出现的:pthread_create。有兴趣的同学可以去了解一下linux的API。点我点我 堆内存相信程序猿们都了解,这里大概说一下栈(stack)是干什么用的:保存局部变量、保存现场、保存函数参数……栈内存的运作方式也真的是按照栈的方式:先进后出,将临时变量逐个压栈,然后按照相反的顺序弹出。 典型的栈溢出会出现在没有写好退出条件的递归调用,相信不少人在学生时期算法课都写过类似这样的代码:

void f(int i) {
    f(++i);
}

整个函数并没有分配任何内存,但却会迅速耗尽栈内存。栈溢出的后果有时很严重,在某些系统上甚至会让系统悄无声息地重启,别问我怎么知道的T_T 栈内存还有一个作用,就是保存线程的现场——给线程留个“快照”(snapshot)。每个处理器在某个时间点只能有1个线程在执行,所有这个处理器上执行的线程按照操作系统的调度算法在自己的时间片内执行。 刚才说到局部变量都保存在栈上,我们先来考虑一下下面这个问题:有2个函数walk()、sit():

void walk() {
    int speed;
    int direction;
...
    return;
}

void sit() {
    int position;
    int duration;
...
    return;
}

现在有2个线程thread1、thread2,分别调用walk()和sit()。当thread1调用walk()并执行后,speed和direction被分配到栈上,恰好在walk()返回前线程切换了,thread2开始执行sit(),并在栈上分配了position、duration变量。这时栈长这样:

duration

position

direction

speed

接下来thread1又被调度了,执行到了return,要清除现场了,弹出栈上分配的内存。然而,栈顶的内存并不是thread1分配的!这下尴尬了。。。把栈顶元素释放了,等thread2继续执行就傻了;释放自己分配的内存?栈不允许釜底抽薪,只能移动栈顶指针。实际上并不会出现这种尴尬的场面,因为每个线程都会有一个自己的栈 :D 盗一张图:

可以看到,线程的栈内存是相互独立的。有人会问,那么在栈上分配的内存就是线程安全的、不用考虑同步吗?是的!

对于java,分配线程栈资源是在调用start()后开始,会调用本地方法创建线程并获取相关资源,然后调用线程的run()方法(说句题外话,不要直接调用run()!这样只会在当前线程执行)。 这就不难理解为什么会在创建线程时出现栈溢出了。操作系统通常不会限制线程数,但栈内存是有限制的。上面错误信息中有:

(stack size 16384 bytes)
(1040KB stack)

这些就是线程栈的大小,跟系统设置有关。有的很小16384字节;有的就大多了1040K字节。注意,这个并不是进程栈大小,进程栈可以通过ulimit -s或ulimit -a查看:

shell@hwmt7:/ $ ulimit -a
time(cpu-seconds)    unlimited
file(blocks)         unlimited
coredump(blocks)     0
data(KiB)            unlimited
stack(KiB)           8192
lockedmem(KiB)       64
nofiles(descriptors) 1024
processes            13326
flocks               unlimited
sigpending           13326
msgqueue(bytes)      819200
maxnice              40
maxrtprio            0
resident-set(KiB)    unlimited
address-space(KiB)   unlimited

这种栈溢出比较少见,想要体验一下的同学可以这么做: 1)写一个运行了就不会轻易退出的线程——比如在run()里写个死循环 2)再写一个循环,不断创建上面线程的实例,并调用start() 3)静静等待吧,如果你的设备线程栈比较小,500多个线程就挂了(比如华为mate 7);如果比较大,可能要3000多个(比如魅族m1) 亲测可用,成果展示:

06-07 17:49:47.209 4229-1169/com.oscaryue.testapp W/libc: pthread_create failed: clone failed: Try again
06-07 17:49:47.210 4229-1169/com.oscaryue.testapp E/aoc: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"
06-07 17:49:47.211 4229-1169/com.oscaryue.testapp W/System.err: java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.lang.Thread.nativeCreate(Native Method)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.lang.Thread.start(Thread.java:1063)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.util.Timer$TimerImpl.<init>(Timer.java:192)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.util.Timer.<init>(Timer.java:365)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.util.Timer.<init>(Timer.java:384)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.util.Timer.<init>(Timer.java:391)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at com.oscaryue.testapp.MyTimer$1.run(MainActivity.java:52)
06-07 17:49:47.230 4229-1169/com.oscaryue.testapp W/System.err:     at java.lang.Thread.run(Thread.java:818)
06-07 17:49:47.235 4229-1169/com.oscaryue.testapp E/AndroidRuntime: FATAL EXCEPTION: Thread-7729
                                                                    Process: com.oscaryue.testapp, PID: 4229
                                                                    java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
                                                                        at java.lang.Thread.nativeCreate(Native Method)
                                                                        at java.lang.Thread.start(Thread.java:1063)
                                                                        at java.util.Timer$TimerImpl.<init>(Timer.java:192)
                                                                        at java.util.Timer.<init>(Timer.java:365)
                                                                        at java.util.Timer.<init>(Timer.java:384)
                                                                        at java.util.Timer.<init>(Timer.java:391)
                                                                        at com.oscaryue.testapp.MyTimer$1.run(MainActivity.java:52)
                                                                        at java.lang.Thread.run(Thread.java:818)
06-07 17:49:50.823 4229-4244/com.oscaryue.testapp W/aoc: Suspending all threads took: 5.334ms
06-07 17:49:51.072 4229-1169/com.oscaryue.testapp I/Process: Sending signal. PID: 4229 SIG: 9

现实中怎么会出现这样的OOM呢?总不能那么赤裸裸地写死循环吧?来来,老司机教你 :P 1)开个线程池,不要设置线程数上限,或者设置成max_int也行 2)巧妙地利用线程同步写个死锁,让线程池里的线程互相等待,永不结束 3)好啦,现在就可以悄悄扩展线程池了,OOM就在不远的前方 而且,触发这个OOM的是恰巧在临界点创建线程的倒霉蛋,作为罪魁祸首的你也不那么容易被揪出来,哈哈 玩笑了,说到最后,大家还是要避免这种问题的发生。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券