Java内存溢出OOM解决方案

什么是OOM

OOM是“Out Of Memory”的缩写,即内存溢出,是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存,导致程序无法继续正常运行。

dump文件和hprof区别

dump文件

  • 定义:dump文件,也被称为内存转储文件或内存快照文件,是进程的内存镜像。
  • 生成时机:通常在程序崩溃、系统干预或发生其他异常时生成。
  • 内容:记录了程序在崩溃或异常发生时的内存状态,包括程序运行的模块信息、线程信息、堆栈调用信息等。

hprof文件

  • 定义:hprof文件是Java虚拟机(JVM)在运行时生成的一种堆转储(heap dump)文件,专门用于记录Java堆的内存使用情况。
  • 生成时机:当JVM检测到内存溢出(如OutOfMemoryError)时,或在性能调优过程中主动触发堆转储时生成。
  • 内容:详细记录了Java堆的内存使用情况,包括对象的引用关系、内存使用量等。

JVM导出dump文件方法

1、jmap命令导出dump文件

## java_pid java进程id,可以用jps命令查看
jmap -dump:live,format=b,file=/home/heap.hprof java_pid

2、java虚拟机参数自动生成

## 当OutOfMemoryError发生时自动生成 Heap Dump 文件。
-XX:+HeapDumpOnOutOfMemoryError
## 指定 dump 文件存储路径
-XX:HeapDumpPath=/home/dumps

什么情况下会出现OOM

堆内存溢出、栈内存溢出、元空间内存溢出、直接内存溢出、老年代内存溢出

堆内存溢出

  • 存放超出堆内存大小的对象了,产生oom,堆内存溢出会被垃圾回收掉,不会影响项目。
  • 什么会存放在堆内存,具体如下:
存放类别 解释
普通对象 通过new关键字创建的对象实例,如new MyClass()创建的MyClass对象,都会被分配到堆内存中。对象的实例字段(属性)和对象本身都存储在堆内存里.
数组对象 包括基本数据类型的数组(如int[]、String[]等)和对象数组(如MyClass[]),它们的元素也存储在堆内存中
对象头 每个对象在堆内存中都有一个对象头,用于存储对象的元数据信息,如对象的哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息,以及指向对象类元数据的指针(Klass指针)
对象引用 对象之间的引用关系(如一个对象的字段是另一个对象的引用)也存储在堆内存中。对象引用本身是一个指向堆内存中对象地址的指针
动态字段 在运行时动态添加到对象的字段(如通过反射机制添加的字段)也会存储在堆内存中.
动态方法 在运行时动态添加到对象的方法(如通过反射机制添加的方法)的相关信息也会存储在堆内存中,但方法的实现代码本身通常存储在方法区(元空间)中.
临时对象 在方法调用过程中创建的临时对象,如局部变量对象、中间计算结果对象等,也会被分配到堆内存中.
克隆对象 通过对象的clone()方法创建的对象克隆也会存储在堆内存中.
序列化对象 通过序列化机制将对象转换为字节流时,序列化对象的数据也会存储在堆内存中,直到被写入到文件或网络传输出去.
反序列化对象 通过反序列化机制将字节流还原为对象时,反序列化生成的对象也会存储在堆内存中.
缓存对象 一些框架或应用程序可能会使用缓存机制将对象存储在堆内存中,以便快速访问和重用这些对象.
池化对象 对于一些频繁创建和销毁的对象,如连接对象、线程对象等,可能会使用对象池技术将这些对象存储在堆内存中,以提高性能和资源利用率.
@RequestMapping("/oom")
@RestController
public class OutOfMemoryController {

    @GetMapping("/doTest")
    public String doTest() {
        List<Object[]> container = new ArrayList<>();

        while(true) {
            container.add(new Object[1024 * 1024 * 10]);
        }
    }
}

image

image

栈内存溢出

  • 栈内存溢出一般情况下都是方法内的应用递归调用过深,导致栈空间好进(因为每次方法调用都会在栈上压一个新的栈针,调用层太多,栈空间耗尽)。
  • 每个栈帧代表一个方法调用,当一个方法被调用时,一个新的栈帧会被创建并压入栈顶,当方法执行完毕后,该栈帧会被弹出栈顶。栈帧主要包含以下内容:
存放类别 解释
局部变量表 用于存储方法的局部变量和方法参数。局部变量表的大小在方法被编译时确定,并存储在方法的字节码中。局部变量可以是基本数据类型、对象引用或返回地址等.
操作数栈 用于存储操作数和中间计算结果。操作数栈的大小也由方法的字节码决定。在方法执行过程中,操作数栈会根据字节码指令进行入栈和出栈操作,以完成各种计算和操作.
动态链接信息 包含当前方法的调用者信息,如调用者的类、方法名、方法描述符等。这些信息用于实现方法调用的动态链接,确保正确调用父类或接口中的方法.
方法返回地址 存储方法执行完毕后返回到的位置。当方法执行完毕后,会根据返回地址跳转到调用者的方法中继续执行.
@RequestMapping("/oom")
@RestController
public class OutOfMemoryController {

    @GetMapping("/doTest")
    public String doTest() {
        repeatExecute();

        return "HelloWorld";
    }

    public void repeatExecute() {
        repeatExecute();
    }
}

image

image

元空间内存溢出

元空间内存主要存放:类的元数据、运行时常量池、类加载器的数据、垃圾回收信息。

存放类别 解释 归类
类的结构信息 包括类的名称、修饰符、超类和接口信息、字段和方法信息等 类的元数据
常量池信息 存储类的常量池中的常量,如字符串常量、类常量、字段常量等。这些常量在类加载过程中被解析和存储. 类的元数据
字段和方法的元数据 包括字段的名称、类型、修饰符等信息,以及方法的名称、参数类型、返回类型、修饰符、字节码等信息 类的元数据
类的继承关系信息 存储类的继承关系,如超类和子类的信息,以及类与接口之间的实现关系 类的元数据
类的静态变量 存储类的静态变量的值。静态变量是类级别的变量,属于类的元数据的一部分 类的元数据
字符串常量池 存储字符串常量,包括字符串字面量和通过intern()方法添加的字符串常量. 字符串常量池在Java 7及以后版本中从永久代移动到了堆内存中,但在元空间中仍然存储一些与字符串相关的元数据信息 运行时常量池
类常量池 存储类的常量池中的其他常量,如类常量、字段常量等. 运行时常量池
本地机器码 JVM的即时编译器(JIT)会将字节码编译成本地机器码,这些编译后的代码也会存储在元空间中,以便快速执行. JIT编译器生成的代码
类加载器的元数据 存储类加载器的元数据信息,如类加载器的名称、父类加载器信息等. 类加载器的数据
已加载类的记录 记录已经加载的类的信息,用于类加载的检查和管理. 类加载器的数据
类的卸载信息 存储类的卸载信息,用于垃圾回收器在类卸载时进行清理和回收. 垃圾回收信息
@RequestMapping("/oom")
@RestController
public class OutOfMemoryController {
    
    @GetMapping("/doTest")
    public String doTest() {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(SelfDefineClass.class);
            enhancer.setCallback(new ProxyDefineClass());
            enhancer.setUseCache(false);
            enhancer.create();
        }
    }
}

image

image

jvisualvm工具分析OOM问题


  • 参照堆内存溢出例子报错,生成堆内存快照(hprof文件)
-Xmx64m
-Xms64m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=F:\\temp\\err.hprof
  • 装入hprof内存快照文件:点击出现oom异常错误的线程链接。

image

  • 找到出错的线程,然后看到提示错误信息,包含所在类、所在方法、代码行号(绿框内)。

image

注意


前面基于jvm内存分布情况分析为JDK8

posted @ 2025-04-26 19:21  sunpeiyu  阅读(272)  评论(0)    收藏  举报