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]);
}
}
}


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


元空间内存溢出
元空间内存主要存放:类的元数据、运行时常量池、类加载器的数据、垃圾回收信息。
| 存放类别 | 解释 | 归类 |
|---|---|---|
| 类的结构信息 | 包括类的名称、修饰符、超类和接口信息、字段和方法信息等 | 类的元数据 |
| 常量池信息 | 存储类的常量池中的常量,如字符串常量、类常量、字段常量等。这些常量在类加载过程中被解析和存储. | 类的元数据 |
| 字段和方法的元数据 | 包括字段的名称、类型、修饰符等信息,以及方法的名称、参数类型、返回类型、修饰符、字节码等信息 | 类的元数据 |
| 类的继承关系信息 | 存储类的继承关系,如超类和子类的信息,以及类与接口之间的实现关系 | 类的元数据 |
| 类的静态变量 | 存储类的静态变量的值。静态变量是类级别的变量,属于类的元数据的一部分 | 类的元数据 |
| 字符串常量池 | 存储字符串常量,包括字符串字面量和通过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();
}
}
}


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

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

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

浙公网安备 33010602011771号