JVM OutOfMemoryError 与 StackOverflowError 异常

目录

前言

堆溢出

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

方法区溢出


前言

        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() {@Overridepublic 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.xdnf.cn/news/142790.html

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

相关文章

鸿蒙next json解析 ArkUI 带你玩转 arkts json解析

前言导读 相信很多同学再开发过程中都会遇到json解析的处理&#xff0c;不管是跟服务端交互 或者是读取本地的json 都会遇到json解析 那么正好今天有空正好讲一下鸿蒙next里面的json解析 JSON解析与生成 本模块提供了将JSON文本转换为JSON对应对象或值&#xff0c;以及将对象…

Mac OS系统如何下载安装Python解释器

目录 Mac安装Python的教程 mac下载并安装python解释器 如何下载和安装最新的python解释器 访问python.org&#xff08;受国内网速的影响&#xff0c;访问速度会比较慢&#xff0c;不过也可以去我博客的资源下载&#xff09; 打开历史发布版本页面 进入下载页 鼠标拖到页面…

【经典文献】双边滤波

文章目录 ICCV 1998基本思路双边高斯滤波 ICCV 1998 1995年&#xff0c;Aurich和Weule提出一种非线性高斯滤波器&#xff0c;三年后&#xff0c;Tomasi和Manduchi将其用于图像平滑&#xff0c;并将其命名为双边滤波。 Aurich, V., & Weule, J. (1995). Non-linear Gaussi…

【C++】list常见用法

&#x1f525;个人主页&#x1f525;&#xff1a;孤寂大仙V &#x1f308;收录专栏&#x1f308;&#xff1a;C从小白到高手 &#x1f339;往期回顾&#x1f339;&#xff1a;[C]vector常见用法 &#x1f516; 流水不争&#xff0c;争的是滔滔不息。 文章目录 一、list的介绍li…

web安全测试入门

参考课程&#xff1a; 04-软件安全测试基础-网络协议基础-网络模型_哔哩哔哩_bilibili 1.软件安全测试概述 安全测试&#xff1a; 安全性测试指有关验证应用程序的安全等级和识别潜在安全性缺陷的过程 导致软件出现安全问题的主要原因或根源是软件的安全漏洞 安全漏洞&#x…

Vue2源码解读

vue源码_哔哩哔哩_bilibili 1.Vue源码路径目录解读 Vue2源码的路径目录被设计得非常清晰&#xff0c;每个文件夹都承担着特定的职责和功能。以下是这些主要文件夹&#xff08;compiler、core、platform、server、sfc、shared&#xff09;的详细解读&#xff1a; 1. compiler …

变压器漏感对整流电路的影响

目录 1. 电压波形畸变 2. 输出电压波动 3. 电流纹波增加 4. 降低整流效率 5. 影响开关器件的性能 6. EMI&#xff08;电磁干扰&#xff09;增加 总结与应对措施 变压器漏感在整流电路中会产生一些影响&#xff0c;尤其在高频应用或电流变化较大的情况下&#xff0c;其影…

【Java】多线程:Thread类并行宇宙

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持&#xff01; 在现代编程中&#xff0c;多线程是提高程序性能和响应能力的一种重要手段。Java 通过 Thread 类和 Runnable 接口提供了丰富的线程管理功能。本文是对 Thread 类基本用法的总结。 线程创建 线程可以…

GEE 案例:利用2001-2024年的MODIS数据长时序ndvi指数归一化后的结果分析

目录 简介 指数 数据 代码 结果 简介 利用2001-2024年的MODIS数据长时序ndvi指数归一化后的结果分析&#xff0c;并加载时序图。 指数 NDVI指数&#xff08;Normalized Difference Vegetation Index&#xff09;是用来评估地表植被覆盖度和健康程度的指数。它通过计算红…

PMP--一模--解题--121-130

文章目录 12.采购管理--投标人会议&#xff08;又称承包商会议、供应商会议或投标前会议&#xff09;是在卖方提交建议书之前&#xff0c;在买方和潜在卖方之间召开的会议&#xff0c;其目的是确保所有潜在投标人对采购要求都有清楚且一致的理解&#xff0c;并确保没有任何投标…

【数据结构取经之路】图解AVL树

目录 AVL树的前世今生 走进AVL AVL树的实现 1、AVL树节点的定义 2、AVL树的插入 3、完整代码 AVL树的性能 AVL树的前世今生 我们知道&#xff0c;普通的二叉搜索树在不少场景下可以提高我们的查找效率。例如下面这种情况&#xff0c;我们要找22。从根开始&#xff0c;…

搭建一个基于角色的权限验证框架

说明&#xff1a;基于角色的权限验证&#xff08;Role-Based Access Control&#xff0c;RBAC&#xff09;框架&#xff0c;是目前大多数服务端的框架。本文介绍如何快速搭建一个这样的框架&#xff0c;不用Shiro、Spring Security、Sa-Token这样的“大框架”实现。 RBAC 基于…

[Meachines] [Medium] Bart Server Monitor+Internal Chat+UA投毒+Winlogon用户密码泄露权限提升

信息收集 IP AddressOpening Ports10.10.10.81TCP:80 $ nmap -p- 10.10.10.81 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 80/tcp open http Microsoft IIS httpd 10.0 | http-methods: |_ Potentially risky methods: TRACE |_http-server-header: Micros…

在沉浸式翻译中使用SiliconCloud API:提升翻译体验

沉浸式翻译&#xff0c;作为广受好评的双语对照网页翻译插件&#xff0c;结合了硅基流动&#xff08;SiliconFlow&#xff09;的大语言模型&#xff0c;为用户提供了快速、准确的跨语言翻译服务。自2023年上线以来&#xff0c;这款AI双语对照网页翻译扩展已帮助超过100万用户跨…

【C++11 —— 异常】

C —— 异常 C语言传统的处理错误的方式C异常概念异常的使用异常的抛出和捕获异常的重新抛出异常安全异常规范 自定义异常体系自定义异常体系的目的 C标准库的异常体系异常的优缺点 C语言传统的处理错误的方式 在C语言中&#xff0c;错误处理通常依赖于返回值和全局变量的方式…

【程序人生】《把时间当做朋友》李笑来思维导图

李笑来&#xff0c;男&#xff0c;朝鲜族&#xff0c;1972年7月12日生&#xff0c;吉林人&#xff0c;畅销书作家、区块链专家、天使投资人。 INBlockchain硬币资本创始人&#xff0c;投资了区块链项目。曾在央视采访中自曝拥有六位数比特币。 出版发行《把时间当做朋友》 [9]、…

LAMP环境搭建记录:基于VM的Ubuntu虚拟机

LAMP环境搭建记录&#xff1a;基于VM的Ubuntu虚拟机 我们从这样的角度出发&#xff1a; 一、简述LAMP环境 二、搭建LAMP环境 一、什么是LAMP环境 首先&#xff0c;该词是复合&#xff1a; ​ LAMP Linux Apache MySQL PHP 其中&#xff0c;逐项简述为&#xff1a; …

代码随想录算法训练营第57天|卡码网 53. 寻宝 prim算法精讲和kruskal算法精讲

1. prim算法精讲 题目链接&#xff1a;https://kamacoder.com/problempage.php?pid1053 文章链接&#xff1a;https://www.programmercarl.com/kamacoder/0053.寻宝-prim.html prim算法 是从节点的角度 采用贪心的策略 每次寻找距离 最小生成树最近的节点 并加入到最小生成树中…

MoCo和SimCLR【CV双雄】

文章目录 文章列表五、MoCo V15.1 文章摘要5.2 实验结果5.3 文章图示图 1: Momentum Contrast (MoCo) 训练方法概述图 2: 三种对比损失机制的比较 六、SimCLR V16.1 文章摘要6.2 文章实验6.3 文章图示图 1: ImageNet Top-1 Accuracy of Linear Classifiers图 2: 对比学习框架图…

MySQL日志binlog和redo log区别

MySQL binlog简介 MySQL中有两类日志&#xff1a;binlog和redo log&#xff0c;分别有不同的作用和解决问题。binlog是归档日志&#xff0c;在MySQL server层的日志&#xff0c;适用于所有存储引擎&#xff0c;redo log是innodb特有日志用于crash-safe时恢复数据。 binlog和r…