JVM OutOfMemoryError 与 StackOverflowError 异常

news2024/11/13 11:55:38

目录

前言

堆溢出

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

方法区溢出


前言

        JVM规范中规定, 除了程序计数器之外, 其他的运行时数据区域, 例如堆栈, 方法区, 都会出现OutOfMemoryError异常.

        那么到底是怎么样的代码, 才会引起堆溢出, 栈溢出, 或者是方法区的溢出呢? 如果遇到了又该如何解决? 这就是我们本节内容的主题. 

        我们在书写代码案例的时候, 会使用JVM参数手动调节堆等内存区域的大小, 方便观察结果.  但是不同的发行版的JVM在相同的参数下可能会出现些许差异. 


堆溢出

        JVM的堆区用于存放类的实例对象. 我们只需要不断的创建对象, 并且避免JVMGC回收这些对象, 久而久之就会将堆的空间沾满, 考虑如下代码: 

    public static void main(String[] args) {
        List<Student> list = new LinkedList<>();

        while (true) {
            list.add(new Student("John", 25));
        }
    }

        为了快速得到结果, 我们需要减小堆的可用空间, 如下: 

  • -verbose:gc   这个参数启用了垃圾收集(Garbage Collection, GC)的详细日志输出
  • -Xms20M    这个参数设置了Java堆的初始大小(Initial Heap Size)为20MB
  • -Xmx20M    这个参数设置了Java堆的最大大小(Maximum Heap Size)为20MB
  • -Xmn10M  这个参数设置了年轻代(Young Generation)的大小为10MB,年轻代是堆内存的一部分,主要用于存放新生成的对象
  • -XX:+HeapDumpOnOutOfMemoryError    这个参数启用了当OutOfMemoryError(内存溢出错误)发生时,自动生成堆内存转储(Heap Dump)的功能

可以在idea中配置JVM参数 , 如下: 

 

勾选JVM选项: 

添加参数: 

运行结果如下: 

显示OOM异常, 然后问题就出在这个LinkedList中. 

注意到这句话:  

Dumping heap to java_pid20664.hprof ...

说明堆转储文件已经生成, 我们去当前项目下的文件里面去查看: 

发现存在这个文件, 我们使用的IDEA, 可以直接点击分析查看是什么地方出了问题, 如下: 

 可以看到排在前面的就是这个LinkedList和Student, 因此可以快速排查问题所在. 

但是我们应该区别一下, 到底是因为内存泄漏还是内存溢出: 

  • 内存泄漏,  创建了很多类的对象, 这类对象不是必要的, 并且这些类对象应该在被使用完之后被垃圾回收器回收, 但是因为某些疏忽导致没有被回收, 此种情况被成为内存泄漏, 一般会引起非常大的故障
  • 内存溢出则跟泄漏相近, 但是内存溢出创建的对象是有必要的, 创建的对象在运行期间一直需要, 但是再进行额外的创建的时候, 因为内存空间不够而抛出OOM异常, 就是内存溢出. 

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

         由于HotSpot虚拟机是不区分虚拟机栈和本地方法栈的, 因此对于HotSPot来说, 设置Xoss参数(设置本地方法栈的大小) 虽然存在, 但实际不会有任何效果, 栈的容量在HotSpot上面只能由-Xss参数来设定, 

        java虚拟机规范中描述了两种异常: 

  • 如果一个线程请求的栈深度大于虚拟机所允许的最大深度, 就会抛出StackOverflowError异常. 
  • 如果虚拟机支持动态栈内存扩展, 也就是在栈空间不足的时候, 继续申请内存, 但是如果由于某些原因不能继续申请足够的空间的时候, 就会抛出: OutOfMemoryError异常

        Java的虚拟机规范中, 没有明确的说明虚拟机必须支持动态栈内存扩展, 并且HotSpot是不支持动态扩展的, 因此在栈内存不足的时候, 是不会继续申请内存的, 那么多的栈帧会因为没有足够空间, 而无法被载入虚拟机栈, 这个时候, 就必须表示这种情况是一种异常情况, 因此就会抛出StackOverflowError异常 

        为了验证这几点, 我们设计几个场景 : 

  1. 减少栈容量, 通过使用JVM参数的形式
  2. 增大每个入栈的栈帧的大小, 通过增加局部变量表的大小的方式

        先来看看第一种, 设置JVM参数: -Xss128k

考虑如下代码: 

public final class Test  {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        Test oom = new Test();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

 这个代码不断的递归, 并且没有递归结尾, 会一直持续下去, 直到溢出. 

第二种是增加局部变量表的大小, 来让每一个栈帧的大小变大, 那么入栈之后, 剩余的空间就会减小的更多(对比正常的栈帧)

考虑如下代码: 

我们只需要在递归的时候, 定义足够多的局部变量就行

public final class Test {
    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) {
        try {
            test();
        } catch (Error e) {
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

输出如下: 

        我们对比一下, 第一种减小栈容量的方法(-Xss128k), 会让main线程在栈调用深度在小于1000的时候就排除栈StackOverflowError溢出异常. 而通过增加变量表的方式, 会在6000多深度的时候出现异常. 

结果表明: 无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常(因为不会动态扩展内存, 所以不会因为无法申请足够的空间而发生OOM异常) 

        但是, 如果在支持动态内存扩展的虚拟机上, 你像上述代码那样, 就会出现不同的结果, 就很有可能会触发OOM异常, 因为新的栈帧因为内存不够的时候, 就会申请新的内存, 但是由于某种限制, 申请失败, 就会抛出OOM异常. 

       

        注意这里的情况仅限于main线程, 也就是单线程, 如果在多线程的情况, 又会有所不同, 很容易李姐, 因为创建线程, 也会消耗内存资源, 总所周知, win32的系统下一个进程最多2GB, 对于一个java进程而言, 出去堆空间, 方法区, 和本地方法栈的空间, 加上程序计数器的空间, 计算如下: 

2GB - 最大堆内存  - 最大方法区内存  - 程序计数器内存  - 直接内存  - 虚拟机进程 = 虚拟机栈 + 本地方法栈内存. 

        因此每个线程分配到的栈内存越大, 也就是一个线程在进行递归调用的时候, 如果递归深度很深导致一个线程的栈帧数量很多, 或者是在固定的栈帧数量的情况下, 每个栈帧的局部变量表的大小太大, 导致一个线程的一个栈帧很大(局部变量表内存占用很大), 导致当前线程的虚拟机栈内存占用很大.  从而削减了其他线程的栈可用空间,  能创建的其他的线程的数量自然就减小. 此时建立线程就更容易因为栈不足而内存溢出. 

        下面的代码就是因为创建线程数过多导致内存溢出. 

 设置虚拟机参数-Xss2M限制栈空间为2M

注意, 不要轻易尝试这个代码, 系统会因为线程数激增而导致系统假死

注意, 不要轻易尝试这个代码, 系统会因为线程数激增而导致系统假死

注意, 不要轻易尝试这个代码, 系统会因为线程数激增而导致系统假死

public class Test {
    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) throws Throwable {
        Test oom = new Test();
        oom.stackLeakByThread();
    }
}

运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

        如果是由于建立过多的线程数导致内存溢出, 在不能减少线程数或者更换64位机器的情况下, 就只能通过减少最大堆或者方法区的容量的方法(上图等式中等号左边的部分)

        这种"减少内存"来避免内存溢出的情况比较少见. 


方法区溢出

         大家应该都忘记这个图了吧, 翻出来给大家看看. 

         学过JVM应该都知道, HotSpot在JDK1.7开始, 去永久代, 并且在JDK1.8中, 使用元空间来代替永久代的故事, 我们就来看看永久代和元空间的区别: 

  • 永久代, 永久代是JVM堆中的一部分, 使用着java堆区的GC方式, 其大小可以在启动时使用-XX:MaxPermSize来这是, 并且是不能动态扩展的, 这也就意味着如果一次性加载大量类, 或者过多的生成了大量的动态类, 就会导致永久代内存溢出, 从而引发: java.lang.OutOfMemoryError: PermGen space错误
  • 元空间: 直接使用本地内存, 其大小可以根据需要进行动态调整, 初始大小和最大内存大小都可以通过虚拟机参数设置,  默认可以扩展至几乎所有的本地内存, 减少了内存溢出的风险. 

        要想常量池溢出, 你只需要往常量池中塞入足够的对象即可, 如下: 

限制容量, 设置JVM参数: -XX:PermSize=6M -XX:MaxPermSize=6M

请以JDK1.6运行. 因为1.7起常量池被移动到java堆中, 限制方法区容量可以说毫无意义.

    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        String str = "hello";
        int i = 0;
        while (true) {
            set.add((str + (i++)).intern());
            str += i;
            System.out.println(str);
        }
    }

        String的intern方法, 它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用。否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用.

        此代码运行结果如下: 

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18

        显示PermGen空间溢出, 也就是永久代OOM. 

        但是可以将堆区的空间限制到6MB即可, 如下: -Xmx6M

        方法区的主要职责就是存放类型相关的信息, 例如泪目, 访问限定修饰符, 常量池, 字段描述方法描述等, 对这部分数据进行测试的主要思路就是产生大量的运行时类去填满方法区, 直到溢出, 这样的场景也很多, 例如Spring框架中的AOP, 代理等, 就是在运行时产生了大量的动态类去增强功能和方法. 

        在元空间替代了永久代之后, 前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了, 但是为了防御破坏性操作, HotSpot还是提供了一些参数作为元空间的防御措施: 

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
  • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2142306.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

书生大模型全链路开源体系,学习

优点 书生浦语开源大模型&#xff0c;是一个开源的大模型&#xff0c;大家可以一起学习 还有配套的教学视频&#xff0c;很快就能上手&#xff0c;而且还奖励算力&#xff0c;可以直接训练&#xff0c;讨论学习&#xff0c;非常nice。 教学视频 书生浦语大模型全链路开源开…

FastAPI与环境变量:实现无缝切换与高效运维

在现代软件开发中&#xff0c;尤其是构建RESTful API时&#xff0c;环境变量的管理显得尤为重要。它们不仅允许我们在不同环境中&#xff08;如开发、测试、生产&#xff09;灵活地调整应用的行为&#xff0c;还极大地增强了应用的安全性和可维护性。FastAPI作为一个新兴的、高…

ROS组合导航笔记1:融合传感器数据

使用机器人定位包&#xff08;robot_localization package&#xff09;来合并来自不同传感器的数据&#xff0c;以改进机器人定位时的姿态估计。 基本概念 在现实生活中操作机器人时&#xff0c;有时我们需要处理不够准确的传感器数据。如果我们想要实现机器人的高精度定位&am…

苍穹外卖 修改nginx的端口后websocket连接失败解决

苍穹外卖 修改nginx的端口后websocket连接失败解决 问题&#xff1a; 后端配置好websocket后前端仍显示如图所示的错误 解决&#xff1a; 先用websocket在线工具测试后端是否能正常连接&#xff08;这个基本上不会出现问题&#xff09;用f12观察前端发送的请求 正常来说这个请…

chatgpt个人版ssrf漏洞

文章目录 免责申明搜索语法漏洞描述漏洞复现修复建议 免责申明 本文章仅供学习与交流&#xff0c;请勿用于非法用途&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任 搜索语法 fofa title"ChatGPT个人专用版"漏洞描述 该系统是一个开源的…

【两方演化博弈代码复现】:双方演化博弈的原理、概率博弈仿真、相位图、单个参数灵敏度演化

目录-基于MatLab2016b实现 一、演化博弈的原理1. 基本概念2. 参与者的策略3.演化过程 二、MATLAB 代码解读&#xff08;博弈参与主体&#xff08;双方&#xff09;策略选择的动态演化讨程&#xff09;三、MATLAB 代码解读&#xff08;博弈主体随着时间策略选择的动态演化讨程&a…

若依nday复现

前言 声明&#xff1a;此文章仅做学习&#xff0c;未经授权严禁转载。请勿利用文章内的相关技术从事非法测试&#xff0c;如因此产生的一切不良后果与文章作者无关 本文章只做简单汇总&#xff0c;在此感谢其他师傅的文章和分享 前置准备 环境搭建 下载&#xff1a;https:/…

访谈心脑血管名医黄力医生:医术精湛,心系患者

黄力医生&#xff0c;一位在心脑血管领域深耕多年的杰出医者&#xff0c;其医学之路同样始于对国内顶尖医学院校的刻苦钻研。在那里&#xff0c;她不仅打下了坚实的医学理论基础&#xff0c;更培养了对医学事业的无限热爱与崇高追求。毕业后&#xff0c;黄力医生毅然选择了心脑…

django-prometheus使用及源码分析

简介 在django服务运行过程中&#xff0c;希望可以对其获取promethues指标进行监控&#xff0c;这样可以实时知道其运行状态&#xff0c;当它运行异常时可以及时进行告警&#xff0c;并且帮助我们可以对其针对性进行优化。比如请求量过大是否要进行限流或者扩容&#xff0c;再…

【黄力医生】血栓隐患大排查:七类人群如何自我监测静脉血栓风险

血栓&#xff0c;这一看似无声无息的健康杀手&#xff0c;实则潜藏着巨大的风险。静脉血栓作为血栓的一种常见类型&#xff0c;其形成与多种因素密切相关&#xff0c;并可能引发严重的并发症&#xff0c;如肺栓塞等。黄力医生指出&#xff0c;有七类人群特别需要关注自身静脉血…

2024/9/16 dataloader、tensorboard、transform

一、pytorch两大法宝元素 假设有一个名为pytorch的包 dir()&#xff1a;用于打开包&#xff0c;看里面的内容 help():用于查看具体的内容的用处 二、python文件&#xff0c;python控制台和jupyter的使用对比 三、pytorch读取数据 pytorch读取数据主要涉及到两个类&#xff1…

Redis——常用数据类型hash

目录 hash常用命令hsethgethdelhkeyshvalshgetallhmgethlenhsetnxhincrbyhdecrby 哈希的编码方式哈希的应用 hash 常用命令 hset HSET key field value [field value ...]//时间复杂度O(1) //返回值&#xff1a;设置成功的键值对的个数hget HGET key field//hdel HDEL key…

数据结构——树(终极版)

树的基本概念&#xff1a; 树的顶部是根节点也是树的入口 父节点&#xff1a;例如&#xff1a;B是F的父节点 子节点&#xff1a;树中的每个节点都可以有0个或多个子节点 叶子节点&#xff1a;像KLFGMIJ这种没有子节点的节点 节点的度&#xff1a;节点的子节点数&#xff1…

新160个crackme - 059-Dope2112.1

运行分析 输入Name和Serial&#xff0c;点击Registrieren按钮&#xff0c;显示疑似错误提示 百度翻译查看一下&#xff0c;发现是德语 PE分析 Delphi程序&#xff0c;32位&#xff0c;无壳 静态分析&动态调试 ida字符串发现正确提示&#xff0c;双击跟进 来到关键函数&…

现在量化中普遍使用QMT和PTrade?哪家可以同时提供QMT/PTrade?

QMT的特点 全面的功能集成&#xff1a; QMT集成了行情显示、策略研究、交易执行和风控管理于一体&#xff0c;为投资者提供了一站式的量化交易解决方案。 高效的交易执行能力&#xff1a; 通过全内存交易实现低延迟的交易执行&#xff0c;单笔延时小于1ms&#xff0c;确保了交易…

秒懂C++之智能指针

目录 前言 智能指针的使用及原理 RAII RAII弊端 std::auto_ptr std::unique_ptr std::shared_ptr shared_ptr弊端 std::weak_ptr 扩展&#xff08;删除器&#xff09; 前言 为了解决抛异常所造成的内存泄漏等问题~秒懂C之异常-CSDN博客~我们来学习智能指针的相关用法…

【图像匹配】基于SIFT算法的图像匹配,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于基于SIFT算法的图像匹配&#xff0c;用matlab实现。 一、案例背景和算法介绍 本…

【MySQL】MySQL中JDBC编程——MySQL驱动包安装——(超详解)

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解Java中JDBC编程&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【MySQL】MySQL索引与事务的透析——&#xff08;超详解&#xff09;-CSDN博客 &#x1f308;感兴趣的小伙伴看一看小编主页&a…

【Linux】初识信号与信号产生

目录 一、认识信号 1 .什么是信号 2 .哪些情况会产生信号 3 . 查看信号 4 . 信号处理 二、产生信号 1 .通过终端按键产生信号 2 .调用系统函数向进程发信号 3 . 由软件条件产生信号 4 . 由硬件异常产生信号 一、认识信号 1 .什么是信号 你在网上买了很多件商品&#xff0c;再…

技术上,如何复现 o1?

知乎&#xff1a;周舒畅链接&#xff1a;https://zhuanlan.zhihu.com/p/720127190 基础模型 搞 o1 首先需要一个基模&#xff0c;这个基模必须是&#xff1a; 能进行“长”生成。注意这和“长 context”不是一回事。模型生成的结果&#xff0c;经常会有自激的噪声存在&#xf…