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异常
为了验证这几点, 我们设计几个场景 :
- 减少栈容量, 通过使用JVM参数的形式
- 增大每个入栈的栈帧的大小, 通过增加局部变量表的大小的方式
先来看看第一种, 设置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(如果设置了的话)的情况下,适当提高该值。
相关文章:

JVM OutOfMemoryError 与 StackOverflowError 异常
目录 前言 堆溢出 虚拟机栈和本地方法栈溢出 方法区溢出 前言 JVM规范中规定, 除了程序计数器之外, 其他的运行时数据区域, 例如堆栈, 方法区, 都会出现OutOfMemoryError异常. 那么到底是怎么样的代码, 才会引起堆溢出, 栈溢出, 或者是方法区的溢出呢? 如果遇到了又该如何…...
linux防火墙学习
Linux 防火墙配置(iptables和firewalld) Linux 防火墙配置(iptables和firewalld)_iptables配置文件位置-CSDN博客 Linux查看防火墙状态及开启关闭命令_linux 查看防火墙-CSDN博客...

Java面试篇基础部分- Java中的阻塞队列
首先队列是一种前进后出的操作结构,也就是说它只允许从队列前端进入,从队列后端退出。这个前端和后端看个人如何理解,也就是通常所说的入队和出队,队头和队尾。 阻塞队列和一般队列的不同就在于阻塞队列是可以阻塞的,这里所说的并不是说队列中间或者队头队尾被拦截了,而是…...
Go语言并发编程之Channels详解
并发编程是Go语言的一大特色,而channel(通道)则是Go语言中用于实现并发的核心工具之一。它源于CSP(Communicating Sequential Processes)的概念,旨在让多个goroutine之间能够高效地进行通信和同步。本文将深入探讨channel的用法、原理和最佳实践,通过丰富的示例代码和详…...
【Java集合】LinkedList
概要 LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。 链表 链表是…...

大模型之基准测试集(Benchmark)-给通义千问2.0做测评的10个权威测基准测评集
引言 在去年(2023)云栖大会上,阿里云正式发布千亿级参数大模型通义千问2.0。据现场介绍,在10个权威测评中,通义千问2.0综合性能超过GPT-3.5,正在加速追赶GPT-4。以下是通义千问在MMLU、C-Eval、GSM8K、HumanEval、MATH等10个主流…...

解决selenium爬虫被浏览器检测问题
文章目录 专栏导读1.问题解析2.代码解析(Edge/Chrome通用)2.1 设置Edge浏览器选项:2.2 尝试启用后台模式2.3 排除启用自动化模式的标志2.4 禁用自动化扩展2.5 设置用户代理2.6 实例化浏览器驱动对象并应用配置2.7 在页面加载时执行JavaScript代码 3.完整代码(可直接…...

计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-17
计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-17 1. Large Language Models in Biomedical and Health Informatics: A Review with Bibliometric Analysis H Yu, L Fan, L Li, J Zhou, Z Ma, L Xian, W Hua, S He… - Journal of Healthcare …, 2024 生物…...

LLM - 理解 多模态大语言模型(MLLM) 的 幻觉(Hallucination) 与相关技术 (七)
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/142463789 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 多模态…...
如何在C++中实现RDP协议的屏幕更新功能?
在C++中实现RDP协议的屏幕更新功能涉及多个步骤,包括接收RDP服务器发送的屏幕更新PDU(协议数据单元)、解析这些PDU以获取图像数据,以及将这些图像数据渲染到本地显示设备上。以下是一个简化的流程,指导你如何在C++中处理这一功能: 1. 监听和接收屏幕更新PDU 首先,你的…...

Tornado 是一个 Python 异步网络库和 web 框架
Tornado 是一个 Python 异步网络库和 web 框架,它最初由 FriendFeed 开发,后来被 Facebook 收购并开源。Tornado 因其非阻塞的 I/O 操作和优秀的性能而广受欢迎,特别是在需要处理大量并发连接的应用中。Tornado 的底层实现主要依赖于 Python …...
鹏哥C语言49---第5次作业:选择语句 if 和 switch
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> //---------------------------------------------------------------------------------第 5 次作业:选择语句 if 和 switch //-----------------------------------------------------------------1.输…...

通过 Flink 的火焰图定位反压
在 Apache Flink 中,Web UI 提供了丰富的监控工具来帮助用户分析和解决作业性能问题,其中火焰图(Flame Graph)是用于分析反压问题的一个强有力的工具。反压可能是由于作业中某些算子处理速度过慢,或者资源耗尽导致的。…...

初识爬虫8
1.selenium的作用和工作原理 2. 使用selenium,完成web浏览器调用 # -*- coding: utf-8 -*- # 自动化测试工具,降低难度,性能也降低 from selenium import webdriverdriver webdriver.Edge()driver.get("https://www.itcast.cn/")…...

Unity SRP 可编程渲染管线的基本用法
可编程渲染管线使用教程 SRP 可以处理Canvas为Screen Space - Overlay的渲染 安装插件 首先进入package manager,下载Core RP Lib组件 创建渲染管线 编写渲染管线逻辑脚本 新建脚本取名为MPipeLine,该脚本用于实现渲染管线的处理逻辑 using Unity…...
AutoX.js向后端传输二进制数据
android的JavaScript自动化软件用过Hamibot和AutoX.js 不过在向后端传输二进制数据时都有些限制,不如浏览器前端那么自由。Hamibot的http按文档应该时能支持传字节数组,但是实际上应该还没有支持。AutoX.js的http也是这样,但是AutoX.js还支持…...

lvgl学习笔记--基础对象1
【LVGL学习笔记】(三)控件使用_学习_煜个头头-GitCode 开源社区 LVGL 基础对象|极客笔记 #include "../../../lv_examples.h"void lv_ex_obj_1(void) {lv_obj_t * obj1;obj1 lv_obj_create(lv_scr_act(), NULL);lv_obj_set_size(obj1, 100, …...

TDengine 在业务落地与架构改造中的应用实践!
前言 在物联网和大数据时代,时序数据的管理和分析变得至关重要。TDengine,作为一款专为时序数据设计的开源数据库,以其卓越的存储和查询效率,成为众多企业优化数据架构的优选。本文将分享我将TDengine成功应用于实际业务的经验&am…...

Python3爬虫教程-HTTP基本原理
HTTP基本原理 1,URL组成部分详解2,HTTP和HTTPS3,HTTP请求过程4,请求(Request)请求方法(Request Method)请求的网址(Request URL)请求头(Request H…...

竹云赋能“中国·贵州”全省统一移动应用平台建设,打造政务服务“新引擎”
近日,2024中国国际大数据产业博览会在贵州贵阳圆满落幕。会上,由贵州省政府办公厅牵头建设的“中国贵州”全省统一移动应用平台正式发布,聚焦民生办事、政务公开、政民互动、扁平高效、数据赋能五大模块,旨在打造公平普惠的服务平…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...

k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
人工智能(大型语言模型 LLMs)对不同学科的影响以及由此产生的新学习方式
今天是关于AI如何在教学中增强学生的学习体验,我把重要信息标红了。人文学科的价值被低估了 ⬇️ 转型与必要性 人工智能正在深刻地改变教育,这并非炒作,而是已经发生的巨大变革。教育机构和教育者不能忽视它,试图简单地禁止学生使…...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...