哪部分区域、什么样的代码和操作可能导致内存溢出异常?(实战:OutOfMemoryError 异常)

本文主要是介绍哪部分区域、什么样的代码和操作可能导致内存溢出异常?(实战:OutOfMemoryError 异常),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java内存区域与内存溢出异常

1. 概述(为什么要去了解虚拟机是怎样使用内存的?)
2. 运行时数据区域(虚拟机中的内存是如何划分的?)
3. HotSpot 虚拟机对象探秘 (HotSpot 虚拟机在 Java 堆中对象是如何创建、如何布局以及如何访问的?)
4. 实战:OutOfMemoryError 异常(哪部分区域、什么样的代码和操作可能导致内存溢出异常?)


4、哪部分区域、什么样的代码和操作可能导致内存溢出异常?

Q:为什么要学习此章内容? 两个目的!
①、通过代码验证 Java 虚拟机规范中描述的各个运行时区域存储的内容;
②、希望读者在工作中遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理

以下代码开头都注释了执行时所需要设置的虚拟机启动参数(注释中“VM Args”后面跟着的参数)。
如何设置虚拟机启动参数?【提醒:点击蓝色字体查看详情!】

以下的代码都是基于 Sun 公司的 HotSpot 虚拟机运行的。

4.1 Java 堆溢出

Q:怎么通过代码去验证此区域存储的内容?
Java 堆用于存储对象实例,所以只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象。当对象数量到达最大堆的容量限制后就会产生内容溢出异常。

根据测试,只要设置最大值 -Xmx 参数即可避免堆自动扩展

import java.util.ArrayList;
import java.util.List;/*** VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError* 限制 Java 堆的大小为 20MB,不可扩展(将堆的最小值 -Xms 参数与最大值 -Xmx 参数设置为一样即可避免堆自动扩展)* 通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。** @author TinyDolphin*         2017/7/5 14:14.*/
public class HeapOOM {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while (true) {list.add(new OOMObject());}}
}

运行结果:
这里写图片描述

结论:
当出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”

怎么解决这个区域的异常呢?
一般的手段:通过内存映像分析工具(如 Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow 即 out of memory)

如果是内存泄露,可进一步通过工具查看泄露对象到 GC Roots 的引用链。于是就能找到泄露对象是通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们的。

如果不存在泄露(内存中的对象确实都还必须存活着),那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

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

由于在 HotSpot 虚拟机中并不区分虚拟机栈本地方法栈,因此,对于 HotSopt来说,虽然 -Xoss 参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由 -Xss 参数设定。

Q:什么情况下,该区域会产生异常?
①、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常
②、如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常

/*** 虚拟机栈和本地方法栈 OOM 测试 * VM Args: -Xss128k* 设置栈内存容量为:128k* @author dolphinzhou**/
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 + "\n");throw e;}}
}

运行结果:
这里写图片描述

结论:
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,虚拟机抛出的都是 StackOverflowError 异常

如果测试时不限于单线程,通过不断的建立线程的方式倒是可以产生内存溢出异常。代码如下:

/*** 创建线程导致内存溢出异常* VM Args: -Xss2M(这时候不妨设置大些) 设置栈内存容量为:2M* @author dolphinzhou**/
public class JavaVMStackOOM {private void dontStop() {while (true) {}}public void stackLeakByThread() {while (true) {Thread thread = new Thread(new Runnable() {public void run() {dontStop();}});thread.start();}}public static void main(String[] args) throws Throwable {JavaVMStackOOM oom = new JavaVMStackOOM();oom.stackLeakByThread();}
}

运行结果: 本人的电脑虚拟机是 64 位,就算设置了很大的栈内存容量,也无法让虚拟机产生下列异常。
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

Q:为什么为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常?
操作系统分配给每个进程的内存是有限制的,譬如 32 位的 windows 限制为 2GB。
剩余的内存为 2GB(操作系统限制) = Xmx(最大堆容量) + MaxPermSize(最大方法区容量) + 程序计数器(消耗内存很小,可以忽略掉)+ 虚拟机进程本身消费的内存 + 虚拟机栈和本地方法栈
所以,每个线程分配到的栈容量越大,可以建立的线程数量就越少,建立线程时就越容易把剩下的内存耗尽

Q:怎么解决这个区域产生的内存溢出?
如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到 1000~2000 完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。
但如果是建立过多的线程导致的内存溢出,在不能减少线程数或者更换 64 位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

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

Q:为什么放在一块进行溢出测试?
因为运行时常量池是方法区的一部分。

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

import java.util.ArrayList;
import java.util.List;/*** VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M* 限制方法区的大小,从而间接限制其中常量池的容量* @author dolphinzhou**/
public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用 List 保持着常量池引用,避免 Full GC 回收常量池行为List<String> list = new ArrayList<String>();// 10MB 的  PermSize 在 integer 范围内足够产生 OOM 了int i = 0;while (true) {list.add(String.valueOf(i++).intern());}}
}

运行结果:JDK1.6下才会报异常
这里写图片描述

结论:
运行时常量池溢出,会报 OutOfMemoryError: PermGen space 错误,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部分。 JDK1.7 中,不会保错。

方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
对于此区域的测试思路: 运行时产生大量的类去填满方法区,直到溢出。

/*** 借助 CGLib 使方法区出现内存溢出异常* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M* @author dolphinzhou*/
public class JavaMethodAreaOOM {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);}});enhancer.create();}}static class OOMObject {}
}

运行结果:

    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) 

方法区溢出也是一种常见的内存溢出异常。
经常生成大量 Class 的场景:程序使用了 CGLib 字节码增强和动态语言、大量 JSP 或动态产生 JSP 文件的应用、基于 OSGi 的应用等。

4.4 本机直接内存溢出

DirectMemory 容量可通过 -XX: MaxDirectMemorySize 指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样。

import java.lang.reflect.Field;
import sun.misc.Unsafe;/*** VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M* @author dolphinzhou**/
public class DirectMemoryOOM {private static final int _1MB = 1024 * 1024;public static void main(String[] args) throws Exception {Field unsafeField = Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe = (Unsafe) unsafeField.get(null);while (true) {unsafe.allocateMemory(_1MB);}}
}

这里写图片描述

这篇关于哪部分区域、什么样的代码和操作可能导致内存溢出异常?(实战:OutOfMemoryError 异常)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/892818

相关文章

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

mysql表操作与查询功能详解

《mysql表操作与查询功能详解》本文系统讲解MySQL表操作与查询,涵盖创建、修改、复制表语法,基本查询结构及WHERE、GROUPBY等子句,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随... 目录01.表的操作1.1表操作概览1.2创建表1.3修改表1.4复制表02.基本查询操作2.1 SE

Java MQTT实战应用

《JavaMQTT实战应用》本文详解MQTT协议,涵盖其发布/订阅机制、低功耗高效特性、三种服务质量等级(QoS0/1/2),以及客户端、代理、主题的核心概念,最后提供Linux部署教程、Sprin... 目录一、MQTT协议二、MQTT优点三、三种服务质量等级四、客户端、代理、主题1. 客户端(Clien

Java中调用数据库存储过程的示例代码

《Java中调用数据库存储过程的示例代码》本文介绍Java通过JDBC调用数据库存储过程的方法,涵盖参数类型、执行步骤及数据库差异,需注意异常处理与资源管理,以优化性能并实现复杂业务逻辑,感兴趣的朋友... 目录一、存储过程概述二、Java调用存储过程的基本javascript步骤三、Java调用存储过程示

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

在Spring Boot中集成RabbitMQ的实战记录

《在SpringBoot中集成RabbitMQ的实战记录》本文介绍SpringBoot集成RabbitMQ的步骤,涵盖配置连接、消息发送与接收,并对比两种定义Exchange与队列的方式:手动声明(... 目录前言准备工作1. 安装 RabbitMQ2. 消息发送者(Producer)配置1. 创建 Spr

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

深度解析Spring Boot拦截器Interceptor与过滤器Filter的区别与实战指南

《深度解析SpringBoot拦截器Interceptor与过滤器Filter的区别与实战指南》本文深度解析SpringBoot中拦截器与过滤器的区别,涵盖执行顺序、依赖关系、异常处理等核心差异,并... 目录Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现

MySQL数据库的内嵌函数和联合查询实例代码

《MySQL数据库的内嵌函数和联合查询实例代码》联合查询是一种将多个查询结果组合在一起的方法,通常使用UNION、UNIONALL、INTERSECT和EXCEPT关键字,下面:本文主要介绍MyS... 目录一.数据库的内嵌函数1.1聚合函数COUNT([DISTINCT] expr)SUM([DISTIN