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

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

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就可以了。

目录
相关文章
|
2月前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
149 52
|
2天前
|
存储 安全 iOS开发
内存卡怎么格式化?6个格式化方法供你选
随着使用时间的增加,内存卡可能会因为数据积累、兼容性或是文件系统损坏等原因需要进行格式化。那么怎样正确格式化内存卡呢?格式化内存卡的时候需要注意什么呢?本文会给大家提供详细的步骤,帮助大家轻松完成格式化内存卡的操作。
|
1月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
57 5
|
11天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
11天前
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
2月前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
55 6
|
2月前
|
存储
栈内存
栈内存归属于单个线程,也就是每创建一个线程都会分配一块栈内存,而栈中存储的东西只有本线程可见,属于线程私有。 栈的生命周期与线程一致,一旦线程结束,栈内存也就被回收。 栈中存放的内容主要包括:8大基本类型 + 对象的引用 + 实例的方法
26 1
|
2月前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
3月前
|
机器学习/深度学习 算法 物联网
大模型进阶微调篇(一):以定制化3B模型为例,各种微调方法对比-选LoRA还是PPO,所需显存内存资源为多少?
本文介绍了两种大模型微调方法——LoRA(低秩适应)和PPO(近端策略优化)。LoRA通过引入低秩矩阵微调部分权重,适合资源受限环境,具有资源节省和训练速度快的优势,适用于监督学习和简单交互场景。PPO基于策略优化,适合需要用户交互反馈的场景,能够适应复杂反馈并动态调整策略,适用于强化学习和复杂用户交互。文章还对比了两者的资源消耗和适用数据规模,帮助读者根据具体需求选择最合适的微调策略。
720 5
|
3月前
|
缓存 监控 Java
在使用 Glide 加载 Gif 动画时避免内存泄漏的方法
【10月更文挑战第20天】在使用 Glide 加载 Gif 动画时,避免内存泄漏是非常重要的。通过及时取消加载请求、正确处理生命周期、使用弱引用、清理缓存和避免重复加载等方法,可以有效地避免内存泄漏问题。同时,定期进行监控和检测,确保应用的性能和稳定性。需要在实际开发中不断积累经验,根据具体情况灵活运用这些方法,以保障应用的良好运行。