39-无限制的调用方法是如何让线程的栈内存溢出的?

898 阅读5分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

1.明确两个异常

在《Java虚拟机规范》 中描述了两种异常:

1) 如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出StackOverflowError异常。

2) 如果虚拟机的栈内存允许动态扩展, 当扩展栈容量无法申请到足够的内存时, 将抛出OutOfMemoryError异常。

《Java虚拟机规范》 明确允许Java虚拟机实现自行选择是否支持栈的动态扩展, 而HotSpot虚拟机的选择是不支持扩展, 所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常, 否则在线程运行时是不会因为扩展而导致内存溢出的, 只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

总结:在我们 Java 开发中,除非当前线程无法申请到足够的内存从而报OOM异常,那么一般绝大多数情况下都是由于栈容量无法容纳新的栈帧而导致的 StackOverflowError 异常。

2.什么原因导致栈内存溢出(Stack Overflow)

1)栈帧过多导致内存溢出, 将抛出StackOverflowError异常。

常见的情况就是递归调用,不断产生新的栈帧,前面的栈帧不释放

我们可以通过以下代码来测试和实验:

对于不同版本的Java虚拟机和不同的操作系统, 栈容量最小值可能会有所限制, 这主要取决于操作系统内存分页大小。 譬如上述方法中的参数-Xss128k可以正常用于32位Windows系统下的JDK 6, 但是如果用于64位Windows系统下的JDK 11, 则会提示栈容量最小不能低于180K, 而在Linux下这个值则可能是228K, 如果低于这个最小限制, HotSpot虚拟器启动时会提示:The Java thread stack size specified is too small. Specify at least 228k****

/**
 * @Description:   VM Args: -Xss128k
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

打印结果:

2)栈帧过大导致内存溢出, 将抛出StackOverflowError异常。

我们这次可以尝试将每一个栈帧的局部变量给多占用一点空间,这样每个栈帧的大小就会变大,我们还是设定每个线程栈空间为128K,看看以下代码运行后,多少次就会撑满内存:

/**
 * @Description: VM Args: -Xss128k
 */
public class JavaVMStackSOF2 {
    private static int stackLength = 0;
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
                unused6, unused7, unused8, unused9, unused10,
                unused11, unused12, unused13, unused14, unused15,
                unused16, unused17, unused18, unused19, unused20,
                unused21, unused22, unused23, unused24, unused25,
                unused26, unused27, unused28, unused29, unused30,
                unused31, unused32, unused33, unused34, unused35,
                unused36, unused37, unused38, unused39, unused40,
                unused41, unused42, unused43, unused44, unused45,
                unused46, unused47, unused48, unused49, unused50,
                unused51, unused52, unused53, unused54, unused55,
                unused56, unused57, unused58, unused59, unused60,
                unused61, unused62, unused63, unused64, unused65,
                unused66, unused67, unused68, unused69, unused70,
                unused71, unused72, unused73, unused74, unused75,
                unused76, unused77, unused78, unused79, unused80,
                unused81, unused82, unused83, unused84, unused85,
                unused86, unused87, unused88, unused89, unused90,
                unused91, unused92, unused93, unused94, unused95,
                unused96, unused97, unused98, unused99, unused100;
        stackLength++;
        test();
    }

    public static void main(String[] args) throws Throwable {
        try {
            test();
        }catch (Error e){
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

打印结果:

我们发现仅51次就撑爆了!

小结:

无论是由于栈帧太大还是虚拟机栈容量太小, 当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

3.解决方案

之前分析Metaspace区域的时候,我们说过建议大家分配为512MB左右的大小,这个大小只要你代码里没有自己胡乱生成类,一般都是绝对足够存放你一个系统运行时所需要的类。

堆内存大小,之前在分析GC的时候也给大家大量的分析过,堆内存一般分配机器内存的一半就差不多了,毕竟还要考虑其他人对内存的使用,以及直接内存的开销等。

那么最后一块内存区域就是我们的栈内存区域,这里我们以线上最基本的一个机器配置4核8G为例:

512MB分配给Metaspace, 4G分配给堆内存(其中包括了年轻代和老年代),剩余就只有3G左右内存了,要考虑到我们的直接内存使用以及操作系统自己也会用掉一些内存。 那么剩余的内存我们分配一两个GB的内存给栈内存就好了

OOM内存分配建议

上图中针对栈内存,为什么设置为1~2G即可,每个线程的大小为1MB就差不多了呢?

假设一个JVM进程内包括他自带的后台线程,依赖的第三方组件的后台线程,加上核心工作线程(比如部署在Tomcat中,那就是Tomcat的工作线程),还要自己可能额外创建的一些线程,加在一起那么就打个1000个线程。

每个线程占据1MB,那1000个线程就需要1GB的栈内存空间,所以一般一个线程分配1MB,整体栈内存大小也基本就是在1G左右,不会超过2G。 比如一个Tomcat,内部所有的线程数量加起来在几百个是比较合理的,也就占据几百MB的内存,线程太多,对于4核CPU的负载也会过高,并不好!

因此,Metaspace区域+堆内存+几百个线程的栈内存,就是JVM一共对机器上的内存资源的一个消耗

所以,如果我们给每个线程的栈内存分配过大的空间,那么会导致机器上能创建的线程数量变少,如果给每个线程的站栈内存相对较小,能创建的线程数也会比较多一些。

当然,一般来说建议给的单个线程栈内存设置1MB就可以了。

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀