【JVM系列】求求别再警报OOM了

🎈接上篇——JVM为开发人猿分担了管理内存的工作,却又时常抛出一些内存泄漏、内存溢出等问题,把我们气得跳脚。我们试图学习Java的内存区域,却惊奇地发现,除了PC外,其他的运行时区域都有 OutOfMemoryError(OOM)异常 !今天,小火种简单聊聊书中对OOM异常的实战部分,以下内容来自对周志明先生 《深入理解JVM》 的个人解读,欢迎指正!


下面我们分区域介绍:

💦Java堆溢出

Java堆溢出是最常见的内存溢出异常。

💞 Java堆主要职责:存储对象实例;

💞 测试思路:大量创建对象,同时避免对象在到达GC Roots时被垃圾回收机制清理,填满最大堆容量限制至溢出。

💞 异常堆栈信息"Java.lang.OutOfMemoryError : Java heap space"

💞 常规解决方案:①使用内存映像分析工具(如 Eclipse Memory Analyzer)分析 Dump 出来的堆转储快照。②确认是内存泄漏还是内存溢出。

dump 一般指将数据导出、转存成文件或静态形式。比如可以理解成:把内存某一时刻的内容,dump(转存,导出,保存)成文件。
☆ 因为程序在计算机中运行时,在内存、CPU、I/O等设备上的数据都是动态的(或者说是易失的),也就是说数据使用完或者发生异常就会丢掉。如果我想得到某些时刻的数据(有可能是调试程序Bug或者收集某些信息),就要把他转储(dump)为静态(如文件)的形式;否则,这些数据永远都拿不到。
☆ 【出自:计算机中的dump到底是什么意思?

  • 内存泄漏(Memory Leak)?可进一步通过工具检查 泄漏对象类型信息(引用路径、关联GC Roots,明确GC无法回收的原因) GC Roots引用链信息(可定位对象创建位置 → 定位内存泄漏代码具体位置)
  • 内存溢出(Memory Overflow)?检查Java虚拟机堆参数(-Xmas, -Xmas)设置: 是否可上调?检查代码:是否存在某对象生命周期过长?持有状态过长?存储结构设计不合理?(应尽量减少程序运行期的内存消耗)

内存泄漏指未及时清理内存垃圾,导致系统无法再提供内存资源(内存资源耗尽);内存溢出指请求分配的内存超出了系统内存,系统不能满足需求,产生溢出。【出自:Java 内存泄漏与内存溢出详解
因此,解决内存溢出问题可以理解为,我们希望内存中的对象都存活。

💦栈溢出

此节不区分虚拟机栈和本地方法栈。

💞 主要职责:线程私有,随Java方法/字节码执行,同步创建栈帧。

💞 测试思路:① 实验范围限制在单线程中,(方法1)使用-Xss参数减少栈容量至 StackOverflowError;(方法2)定义大量本地变量,增大该方法帧中本地变量表的长度至 StackOverflowError;② 创建线程至 OutOfMemoryError 。

💞 异常堆栈信息"Java.lang.OutOfMemoryError : unable to create native thread""possibly out of memory or process/resource limits reached"

☆ 《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展。
☆ HotSpot虚拟机选择不支持(即不会在线程运行时因扩展而溢出),其 OutOfMemoryError异常 只可能在创建内存时发生,其 StackOverflowError异常 只可能因为栈容量无法容纳新栈帧(栈帧太大/虚拟机容量太小)

书中表示:因建立线程过多而导致的内存溢出异常与栈空间是否足够不存在直接关系,而是取决于操作系统本身的内存使用状态,甚至 给每个线程的栈分配的内存越大, 越容易导致OOM异常 。可以这样理解:

  • 操作系统限制每个进程的内存,需分配:Java堆、方法区、PC、直接内存、虚拟机进程内耗、虚拟机栈、本地方法栈。
  • JVM可以通过参数控制Java堆和方法区的最大值;
  • PC消耗内存很小,可忽略;
  • 假设忽略直接内存和虚拟机进程内耗
  • 那么栈内存(包括虚拟机栈和本地方法栈)的扩展将直接影响到可建立的线程空间(反比):线程栈内存拥有越大空间,可建立的线程数量越少,建立线程更快耗尽内存,导致OOM。

💞 举措小结

  • StackOverflowError 异常:线程请求的栈深>虚拟机所允许最大深度。 【举措:含明确错误堆栈 → 容易直接定位】
  • OutOfMemoryError 异常:(虚拟机栈内存允许动态扩展)扩展栈容量无法申请到足够内存。【场景:建立过多线程;举措:在不减少线程数或更换64位虚拟机的情况下 → 减少内存(最大堆或栈容量)以换取更多线程】

💦方法区和运行时常量池溢出

运行时常量池是方法区的一部分。

💞 方法区主要职责: 存放类型相关信息(如类名、访问修饰符、常量池、字段描述、方法描述等)

💞 测试思路:运行时产生大量类以填满方法区至溢出

💞 实际场景:当前主流框架如 Spring、Hibernate 对类进行增强时,都会使用 CGLib 等字节码技术,增强类越多,对方法区空间的需求越大(以保证动态生成的新类型可载入内存)。

💞 异常堆栈信息"java.lang.OutOfMemoryError : PermGen space"
💞 "java.lang.OutOfMemoryError : Java heap space"(JDK7起,字符串常量池被移至Java堆中,所以JDK7以上版本,限制方法区的办法将无法解决该问题)

💞 Hotspot举措:Hotspot 提供了参数作为元空间的防御措施

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认-1(即不限制/仅受限于本地内存);
  • -XX:MetaspaceSize:指定云空间的初始空间大小,以字节为单位,用于触发垃圾收集进行类型卸载,同时收集器对该值适时调整;
  • -XX:MinMetaspaceFreeRatio(-XX:MaxMetaspaceFreeRatio):垃圾收集后控制(最小/最大)元空间剩余容量百分比。

💦本机直接内存溢出

上一篇提到过:

☆ 直接内存 (Direct Memory)并不属于运行时数据区域,但因频繁使用,且该部分内存不受限制,可能会意外导致动态扩展,出现 OutOfMemoryError (OOM) 异常状况。

直接内存容量通过 -XX:MaxDireactMemorySize 参数控制,默认与Java堆最大值 (-Xmx) 一致。
OutOfMemoryError异常并非向操作系统申请内存时抛出,而是通过计算得知并抛出,真正申请分配内存的方法是 Unsafe :: allocateMemory()

💞 异常特征: Heap Dump文件无明显异常情况;Dump文件很小,且程序直接/间接使用了DirectMemory(NIO是典型间接使用),请考虑直接内存溢出。


总结一下

本文分享了如何寻找内存异常问题的根本原因,更多详情还是建议大家去《深入理解JVM》书中一探究竟,不过仅作了解的话,我相信我也复述清楚了😃。JVM的异常信息还算详细,通过异常堆栈信息可以大致区分出是哪部分内存区域出现问题了。看问题总是要从根本找原因,只有理解这些模块为什么这样设计,就会明白系统为什么这样运转,才会考虑如何改进设计使其更加完美。愿我们始终是个热爱思考、保持天真的少年🎈!
(完)

下期预告:小火种要学习垃圾收集器啦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值