vlambda博客
学习文章列表

JVM之虚拟机栈和本地方法栈溢出

上一篇文章讲了一些Java堆发生内存溢出的情况,这篇讲一下虚拟机栈和本地方法栈溢出。
由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说虽然-Xoss参数(设置本地方方法栈大小)存在,但实际上是无效的,栈容量只由-X搜索参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种情况,看似更加严谨,但却存在着互相重叠的地方:当无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质只是对同一问题的两种描述而已。下面运行如下代码:
/** * -Xss128k */public class JavaVMStackSOF {    private int stackLength = 1; private void stackLeak() { stackLength++; stackLeak();    } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable throwable) { System.out.println("stack length : " + oom.stackLength); throw throwable; } }}
运行结果如下:

图1 虚拟机栈SOF

实验结果表明:使用-Xss参数减少栈内存,在单个线程的情况下,由于虚拟机栈内存较小,当内存无法分配时,虚拟机报出的是StackOverflowError。


如果测试不限于单线程,通过不断创建线程的方式,是可以产生内存溢出异常的,如下代码所示:

public class JavaVMStackOOM { private void dontStop() { while (true) { } } private void stackLeakByThread() { while (true) { Thread thread = new Thread(() -> dontStop()); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); }}
但是这样产生的OOM异常与栈空间是否足够大没有直接联系,甚至,为每个线程分配的栈内存越大,越容易出现内存溢出异常。原因不难理解,系统为每个进程分配的内存大小是有限的,比如64位的Windows系统限制为4GB,虚拟机提供了参数来设置Java堆和方法区这两部分的最大内存值,剩余的内存为4GB减去-Xms(Java堆内存),再减去MaxPermSize(方法区最大内存),程序计数器消耗的内存很小,几乎可以忽略掉。因此,每个线程分配的栈容量越大,能创建的线程数就越小,建立的线程就越容易把剩下的内存耗尽。
如果使用虚拟机默认参数,栈深度大多数情况下达到1000-2000完全没有问题,对于正常的方法调用(包括递归)是完全够用的。但是如果是建立过多线程导致的内存溢出,在不能减少线程数的情况下,只能减少最大堆内存和最大栈容量来换取更多线程。