深入理解Java中的OutOfMemoryError(OOM)异常

导言:
在Java开发中,我们经常会遇到程序抛出OutOfMemoryError异常的情况,这意味着程序在运行时无法继续分配所需的内存。这篇博客将深入探讨Java中的OOM异常,包括异常的种类、常见的引起OOM的原因以及如何诊断和处理这些问题。

1. OutOfMemoryError异常简介

OutOfMemoryError是Java中的一个运行时异常,通常指示Java虚拟机(JVM)无法为新的对象分配内存。当程序尝试创建新的对象,而堆内存空间不足以容纳这些对象时,就会抛出OOM异常。OOM异常被分为多个子类,每个子类对应着不同的内存分配问题。

2. OOM异常的种类

2.1. Java Heap Space

这是最常见的OOM异常,表示Java堆内存空间不足。可能的原因包括对象过多、内存泄漏等。

2.2. Java GC Overhead Limit Exceeded

表示垃圾收集器花费的时间过多,但回收的内存量很少。这可能是因为系统的大部分时间都用于垃圾回收,而几乎没有可用于应用的内存。

2.3. PermGen Space

在Java 7及之前版本中,用于存放类的元数据(如类名、方法名)的永久代,可能导致这种类型的OOM异常。

2.4. Metaspace

在Java 8及之后版本中,永久代被元空间(Metaspace)替代。Metaspace存放类的元数据,它不再有固定的大小,而是依赖于系统的可用内存。

3. 最常见的OOM情况:

3.1. java.lang.OutOfMemoryError: Java heap space

java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。

3.2. java.lang.OutOfMemoryError: PermGen space

java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。

3.3. java.lang.StackOverflowError

不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

4. 常见引起OOM的原因

4.1. 内存泄漏和内存溢出

内存被应用程序使用的太多,而且用完后没有释放,浪费了内存。
内存泄漏:应用进程申请并使用完的内存,没有被释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为当前的申请者不用了,但又不能被JVM虚拟机分配给其他申请者。
内存溢出:申请的内存超出了JVM虚拟机能提供的内存大小。

4.2. 大量对象创建

如果程序在短时间内频繁地创建大量对象,而内存又不能及时释放,就容易导致堆内存溢出。

4.3. 大对象

创建一个过于庞大的对象,可能导致堆内存无法容纳。

4.4. 无限递归

在递归调用中没有明确的结束条件,导致递归深度过深,可能导致栈空间溢出。

5. 诊断和处理OOM异常

5.1. 使用堆栈跟踪信息

当程序抛出OOM异常时,查看异常堆栈信息可以帮助定位问题的源头。关注异常发生时的代码行数和方法调用,以便更好地理解问题。

5.2. 使用内存分析工具

工具如VisualVM、MAT(Memory Analyzer Tool)等可以帮助分析Java应用的内存使用情况,查找内存泄漏和大对象等问题。

5.3. 优化代码

通过代码审查和性能优化,减少不必要的对象创建和内存占用。

5.4. 调整JVM参数

根据应用的需求和硬件环境,适当调整JVM的堆大小、永久代大小等参数。

6.补充知识:JVM内存模型

Java虚拟机所管理的内存包括以下 7个 运行时数据区域:

6.1. 程序计数器 (Program Counter Register)
  • 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
  • 线程私有的内存
  • 值得注意的是:《Java虚拟机规范》中,唯一一个没有规定任何OutOfMemoryError情况的区域!!!
6.2. Java虚拟机栈 (VM Stack)
  • Java方法执行的线程内存模型
  • 为虚拟机执行Java方法(也就是字节码)服务
  • 线程私有的内存 其生命周期与线程相同
  • 每个Java方法的执行对应着一个栈帧的进栈和出栈的操作

两类异常:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常
6.3. 本地方法栈 (Native Method Stacks)
  • 区别于 “Java虚拟机栈” :本地方法栈只为虚拟机使用到的本地(Native)方法服务,为其运行提供内存环境
  • 同 “Java虚拟机栈” 一样,本地方法栈也有两类异常:
    • 栈深度溢出时,将抛出StackOverflowError异常
    • 栈扩展失败时,会抛出OutOfMemoryError异常
6.4. Java堆 (Java Heap)
  • 虚拟机所管理的内存中最大的一块
  • Java堆是被所有线程共享的一块内存区域
  • 唯一的目的:存放对象示例。
    • Java中 “几乎” 所有的对象实例都在这里分配内存;
    • 但是,由于现在技术发展,说 “Java对象示例都分配在堆上” 也渐渐变得不是那么绝对了。
  • Java堆是垃圾收集器管理的内存区域,也称 “GC堆”
  • Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该是被视为连续的。
  • 如果在Java堆中没有内存完成实例分配,并且Java堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
6.5. 方法区(Method Area)
  • 和 “Java堆” 一样,是被所有线程共享的一块区域。
  • 在《Java虚拟机规范》中,把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作 “非堆” ,目的是与Java堆区分开来。
  • 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
6.6. 运行时常量池 (Running Constant Pool)
  • 运行时常量池是方法区的一部分
  • 常量池表:用于存放编译期生成的各种字面量与字符引用。
    • 这部分内容将在类加载后存放到方法区的运行时常量池中。
  • 运行时常量池相对Class文件常量池的一个重要特征是具备动态性。
  • 当常量池无法再申请到内存时,会抛出OutOfMemoryError异常
6.7. 直接内存 (Direct Memory)
  • 既不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 但是这部分内存区域也被频繁地使用,而且也可能导致OutOfMemoryError异常出现
    • 在JDK 1.4中新加入了NIO(New
      Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库
      直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
    • 在本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

7. 总结

在Java应用开发中,OOM异常是一个常见但严重的问题。了解不同类型的OOM异常、常见的引起OOM的原因以及诊断和处理的方法,有助于开发人员更好地优化程序,提高系统的稳定性和性能。通过使用合适的工具和技术手段,我们可以更好地应对OOM异常,确保应用在长时间运行中保持稳定。

你可能感兴趣的:(jvm,java,开发语言,jvm)