前言
JVM运行时数据区是Java语言跨平台、自动内存管理的核心基石,也是生产环境中OutOfMemoryError(简称OOM)异常的根源所在。多数Java开发者仅对Java堆有基础认知,对其他内存区域的规范、边界、OOM触发逻辑一知半解,导致线上OOM发生时无法快速定位根因,甚至出现误判、误修的情况。
一、JVM运行时数据区权威规范与底层实现
JVM运行时数据区分为线程私有区域 与线程共享区域两大类,其生命周期、内存边界、异常类型均有明确的虚拟机规范定义,核心架构如下:

1.1 线程私有区域
线程私有区域随线程创建而分配,随线程销毁而回收,不存在线程安全问题,每个区域的职责与异常边界完全独立。
1.1.1 程序计数器(Program Counter Register)
程序计数器是一块极小的内存空间,也是JVM规范中唯一不会抛出任何OutOfMemoryError的内存区域。
-
规范定义:每个线程拥有独立的程序计数器,线程之间互不影响,是线程私有的内存区域。
-
核心作用:存储当前线程正在执行的字节码指令的地址(行号),字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等核心功能都依赖该计数器实现。
-
特殊规则:当线程执行的是Native本地方法时,程序计数器的值为undefined(未定义),因为Native方法的执行由操作系统底层实现,JVM无法追踪其执行地址。
-
无OOM的核心原因:程序计数器的大小在编译期就已确定,仅存储一个指令地址,不会随程序运行动态扩展,因此永远不会出现内存不足的情况。
1.1.2 Java虚拟机栈(Java Virtual Machine Stack)
Java虚拟机栈是Java方法执行的内存模型,线程私有,生命周期与线程完全一致。
-
核心结构 :每个方法执行的同时,JVM会同步创建一个栈帧(Stack Frame) ,栈帧中存储了方法的局部变量表、操作数栈、动态链接、方法返回地址四大核心信息。每个方法从调用到执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
-
局部变量表:存储方法内的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,非对象本身)、returnAddress类型,其大小在编译期就已完全确定,运行期不会改变。
-
操作数栈:方法执行过程中的计算工作台,所有字节码指令的计算操作都在操作数栈中完成,入栈出栈逻辑与数据结构中的栈完全一致。
-
动态链接:将Class文件中的符号引用转换为运行期的直接内存地址,实现方法的动态调用。
-
方法返回地址:方法执行完成后,回到上层调用方法的执行地址,保证程序正常执行。
-
-
异常类型与触发场景 :JVM规范为该区域定义了两类异常:
-
StackOverflowError:当线程请求的栈深度超过JVM允许的最大深度时抛出,最常见的场景是无限递归、方法嵌套层级过深。
-
OutOfMemoryError:如果JVM虚拟机栈支持动态扩展,当扩展时无法申请到足够的内存时抛出。注意:HotSpot虚拟机的实现中,虚拟机栈的大小是固定的(通过-Xss参数设置),不支持动态扩展,因此HotSpot中该区域只会出现StackOverflowError,不会出现OOM,只有创建新线程时,为新线程分配栈内存失败时才会抛出OOM。
-
-
(StackOverflowError)
package com.jam.demo.jvm.oom;
import lombok.extern.slf4j.Slf4j;
/**
-
StackOverflowError 复现示例
-
VM参数:-Xss128k 缩小栈容量,快速复现
-
@author ken
-
@date 2026-03-11
*/
@Slf4j
public class SOEDemo {
private static int stackDepth = 0;/**
- 无限递归方法,触发栈深度溢出
*/
public static void infiniteRecursion() {
stackDepth++;
infiniteRecursion();
}
public static void main(String[] args) {
try {
infiniteRecursion();
} catch (StackOverflowError e) {
log.error("栈深度溢出,当前栈深度:{}", stackDepth, e);
}
}
} - 无限递归方法,触发栈深度溢出
-
1.1.3 本地方法栈(Native Method Stack)
本地方法栈与Java虚拟机栈的职责、结构完全一致,核心区别在于:Java虚拟机栈为Java方法(字节码)服务,而本地方法栈为Native本地方法服务。
-
HotSpot实现细节:HotSpot虚拟机将本地方法栈与Java虚拟机栈合二为一,使用同一个-Xss参数设置容量,因此其异常规则与Java虚拟机栈完全一致,同样会抛出StackOverflowError与OutOfMemoryError。
-
注意事项:Native方法的执行由操作系统底层实现,不受JVM垃圾回收管理,Native方法中申请的本地内存需要手动释放,否则会导致操作系统内存泄漏,间接引发JVM OOM。
1.2 线程共享区域
线程共享区域随JVM启动而创建,随JVM退出而销毁,所有线程共享该区域的内存,是垃圾回收的核心区域,也是OOM异常的高发区。
1.2.1 Java堆(Java Heap)
Java堆是JVM中最大的一块内存区域,线程共享,JVM启动时创建,唯一核心目的是存储对象实例与数组,《Java虚拟机规范》明确规定:所有的对象实例以及数组都应当在堆上分配。
-
实现细节:随着JIT编译技术的发展,标量替换、栈上分配等优化技术使得对象不一定必须在堆上分配,但其逻辑分配地址仍属于堆,不影响规范定义。
-
垃圾回收核心:Java堆是垃圾收集器(GC)管理的核心区域,因此也被称为"GC堆"。JDK 17默认使用G1垃圾收集器,将堆划分为多个大小相等的Region,逻辑上分为年轻代(Eden区、Survivor区)与老年代,分代回收的核心逻辑不变。
-
内存参数控制 :
-
-Xms:设置堆的初始内存大小
-
-Xmx:设置堆的最大内存大小
-
生产环境建议将-Xms与-Xmx设置为相同值,避免堆内存动态扩展带来的性能开销与内存碎片。
-
-
OOM触发场景 :当堆中没有足够的内存完成对象实例分配,并且堆无法再扩展时,会抛出
java.lang.OutOfMemoryError: Java heap space异常,这是生产环境最常见的OOM类型。 -
(Java堆OOM)
package com.jam.demo.jvm.oom;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;import java.util.List;
/**
-
Java堆OOM复现示例
-
VM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
-
@author ken
-
@date 2026-03-11
/
@Slf4j
public class HeapOOMDemo {
/*- 1MB大小的字节数组,快速占用堆内存
*/
private static final byte[] ONE_MB_ARRAY = new byte[1024 * 1024];
public static void main(String[] args) {
List<byte[]> byteList = Lists.newArrayList();
int count = 0;
try {
while (true) {
byteList.add(ONE_MB_ARRAY.clone());
count++;
}
} catch (OutOfMemoryError e) {
log.error("Java堆OOM,累计分配内存:{}MB", count, e);
if (!CollectionUtils.isEmpty(byteList)) {
byteList.clear();
}
}
}
} - 1MB大小的字节数组,快速占用堆内存
-
1.2.2 方法区(Method Area)
方法区是线程共享的内存区域,JVM启动时创建,核心用于存储类的元数据信息,《Java虚拟机规范》定义其存储内容包括:运行时常量池、类的字段与方法数据、构造函数与普通方法的字节码内容、注解信息、泛型信息等。
-
永久代与元空间的核心区别:这是开发者最容易混淆的知识点,二者是方法区在不同JDK版本的实现,核心差异如下: | 特性 | 永久代(PermGen) | 元空间(Metaspace) | |------|-------------------|----------------------| | 适用版本 | JDK 7及之前 | JDK 8及之后(含JDK 17) | | 内存位置 | JVM堆内存内部 | 操作系统本地内存(堆外内存) | | 容量限制 | 受-XX:MaxPermSize参数限制,默认最大64MB | 默认受本机物理内存限制,可通过-XX:MaxMetaspaceSize设置上限 | | 垃圾回收 | Full GC时回收,回收条件苛刻,需类加载器失效 | 类加载器失效时即可回收,回收效率更高,支持并发卸载 | | OOM核心场景 | 大量类加载、动态生成类导致永久代占满 | 动态生成大量类、类加载器泄漏导致元空间占满 |
-
运行时常量池:是方法区的一部分,Class文件中的常量池表(存储字面量、符号引用)会在类加载后存入运行时常量池。注意:HotSpot虚拟机在JDK 7中将字符串常量池(String Table)从方法区移到了Java堆中,JDK 8及之后,运行时常量池的其他内容(符号引用、类/方法/字段的元数据)仍存储在元空间中。
-
OOM触发场景 :当方法区无法满足新的类元数据内存分配需求时,会抛出
java.lang.OutOfMemoryError: Metaspace异常,核心场景为动态生成大量类、类加载器泄漏、元空间容量设置过小。 -
(元空间OOM)
package com.jam.demo.jvm.oom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;/**
-
元空间OOM复现示例
-
VM参数:-XX:MaxMetaspaceSize=20m --add-opens java.base/java.lang=ALL-UNNAMED
-
@author ken
-
@date 2026-03-11
/
@Slf4j
public class MetaspaceOOMDemo {
/*- 无限循环动态生成类,填充元空间
*/
public static void generateClassInfinite() {
int classCount = 0;
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTargetClass.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
enhancer.create();
classCount++;
if (classCount % 1000 == 0) {
log.info("已生成动态类数量:{}", classCount);
}
}
}
public static void main(String[] args) {
try {
generateClassInfinite();
} catch (OutOfMemoryError e) {
log.error("元空间OOM", e);
}
}/**
- 被代理的目标类
*/
public static class OOMTargetClass {
}
}
- 无限循环动态生成类,填充元空间
-
1.3 非规范内存区域:直接内存(Direct Memory)
直接内存不是《Java虚拟机规范》中定义的运行时数据区,也不属于JVM管理的内存,但在Java NIO编程中被频繁使用,也是生产环境OOM的高发区,必须纳入内存管理体系。
-
核心定义:JDK 1.4引入NIO后,提供了基于Channel与Buffer的IO模型,可通过Native函数库直接分配操作系统本地内存,然后通过Java堆中的DirectByteBuffer对象作为该内存的引用进行操作,避免了Java堆与本地内存之间的数据拷贝,大幅提升IO操作性能。
-
容量控制:直接内存的最大容量可通过-XX:MaxDirectMemorySize参数设置,默认值与JVM堆的最大值-Xmx一致。
-
垃圾回收特性:直接内存不受JVM GC管理,只有当Java堆中的DirectByteBuffer对象被GC回收后,才会触发对应的直接内存释放,若出现对象泄漏,会导致直接内存持续占用,最终引发OOM。
-
OOM触发场景 :当直接内存占用达到最大值,无法申请到新的内存时,会抛出
java.lang.OutOfMemoryError: Direct buffer memory异常,核心场景为频繁分配大的直接内存、直接内存泄漏、容量设置过小。 -
(直接内存OOM)
package com.jam.demo.jvm.oom;
import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;import java.lang.reflect.Field;
/**
-
直接内存OOM复现示例
-
VM参数:-XX:MaxDirectMemorySize=20m --add-opens java.base/sun.misc=ALL-UNNAMED
-
@author ken
-
@date 2026-03-11
*/
@Slf4j
public class DirectMemoryOOMDemo {
private static final int ONE_MB = 1024 * 1024;public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
int count = 0;
try {
while (true) {
unsafe.allocateMemory(ONE_MB);
count++;
if (count % 10 == 0) {
log.info("已分配直接内存:{}MB", count);
}
}
} catch (OutOfMemoryError e) {
log.error("直接内存OOM,累计分配:{}MB", count, e);
}
}
}
-
二、OOM异常类型全解析与根因预判
JVM抛出的每一种OOM异常都有明确的错误信息,对应特定的内存区域与根因场景,掌握异常类型与根因的对应关系,是快速定位问题的核心前提。
| OOM异常信息 | 对应内存区域 | 核心根因预判 |
|---|---|---|
java.lang.OutOfMemoryError: Java heap space |
Java堆 | 1. 堆内存设置过小;2. 内存泄漏(对象无法被GC回收);3. 大对象/频繁创建短期对象导致堆占满;4. 数据查询无分页,一次性加载大量数据 |
java.lang.OutOfMemoryError: Metaspace |
方法区(元空间) | 1. 元空间容量设置过小;2. 动态生成大量类(CGlib、动态代理、ASM);3. 类加载器泄漏,类无法被卸载;4. 第三方包反射生成大量类 |
java.lang.OutOfMemoryError: GC overhead limit exceeded |
Java堆 | GC耗时超过98%,但回收的内存不足2%,JVM强制抛出异常,本质是堆内存即将占满,频繁Full GC,是内存泄漏的前兆 |
java.lang.OutOfMemoryError: Unable to create new native thread |
虚拟机栈/本地方法栈 | 1. 系统线程数达到上限;2. 栈内存-Xss设置过大,剩余内存无法创建新线程;3. 线程池无核心线程数限制,无限创建线程 |
java.lang.OutOfMemoryError: Direct buffer memory |
直接内存 | 1. 直接内存容量设置过小;2. 直接内存泄漏(Netty ByteBuf未释放、DirectByteBuffer未被GC回收);3. 频繁分配大的直接内存 |
java.lang.StackOverflowError |
虚拟机栈/本地方法栈 | 1. 无限递归;2. 方法嵌套层级过深;3. 方法内局部变量过多,栈帧过大 |
三、OOM根因定位标准化全流程
OOM定位的核心是保留完整现场,按照标准化流程逐步排查,避免盲目猜测,以下流程可覆盖99%的生产OOM场景,可直接落地执行。

3.1 第一步:锁定OOM异常类型
通过异常堆栈的错误信息,精准匹配上文中的异常类型对照表,快速锁定出问题的内存区域,缩小排查范围,这是定位的核心前提。禁止在未明确异常类型的情况下盲目分析堆dump。
3.2 第二步:保留并收集完整现场数据
OOM发生后,绝对禁止第一时间重启服务,重启会导致所有现场数据丢失,无法定位根因。必须先收集以下完整数据,再执行重启操作:
-
完整的异常堆栈日志:包含异常类型、错误信息、触发的代码位置,是最基础的排查依据。
-
JVM堆dump文件 :堆内存的完整快照,是定位堆OOM的核心数据。生产环境必须提前配置JVM参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvm/heapdump_${PID}.hprof,OOM发生时自动生成dump文件,带上PID避免文件覆盖。若未提前配置,可通过命令jmap -dump:format=b,file=heapdump.hprof <pid>手动生成。 -
GC日志 :记录了JVM的GC时间、频率、回收内存大小、各代内存变化,是判断内存泄漏、GC异常的核心依据。JDK 17生产环境必须配置GC日志参数:
-Xlog:gc*:file=/var/log/jvm/gc_%t.log:time,uptime:filecount=10,filesize=100M,实现日志滚动,避免单个文件过大。 -
线程dump文件 :记录了所有线程的状态、调用栈、锁信息,用于排查线程泄漏、死锁、阻塞问题,通过命令
jstack <pid> > threaddump.txt生成。 -
基础环境信息:JVM启动参数、JDK版本、操作系统版本、物理内存大小、CPU核心数、应用业务日志。
3.3 第三步:基于异常类型缩小排查范围
根据异常类型,直接锁定排查方向,避免无效操作:
-
堆OOM/GC overhead limit exceeded:核心分析堆dump文件,查找内存泄漏的对象、大对象、引用链。
-
元空间OOM:核心分析类加载情况,查看是否有大量动态生成的类、类加载器泄漏。
-
无法创建新线程OOM:核心分析线程dump,查看线程数量、线程状态,是否有线程池无限创建线程。
-
直接内存OOM:核心分析NIO相关代码,查看是否有ByteBuf未释放、DirectByteBuffer泄漏。
-
StackOverflowError:核心查看异常堆栈,找到无限递归的方法位置。
3.4 第四步:使用工具深度分析现场数据
3.4.1 命令行工具(JDK自带,生产环境首选)
JDK自带的命令行工具无需额外安装,可直接在生产服务器上执行,快速获取JVM运行状态,是初步排查的首选工具。
| 工具 | 核心作用 | 常用命令 |
|---|---|---|
| jps | 查看JVM进程ID与主类名 | jps -l 输出完整的主类名与进程ID |
| jstat | 实时查看JVM GC情况、内存使用情况 | jstat -gc <pid> 1000 10 每秒输出一次GC信息,共10次 |
| jstack | 生成线程dump,查看线程状态、死锁、阻塞 | jstack <pid> > threaddump.txt 输出线程dump到文件 |
| jhsdb | JDK 9+引入的多功能工具,替代jmap、jhat,支持堆dump分析、JVM内部状态查看 | jhsdb jmap --dump --format=b --file=heapdump.hprof <pid> 生成堆dump |
3.4.2 可视化工具(深度分析首选)
-
Eclipse Memory Analyzer (MAT) :业界最主流的堆dump分析工具,支持GB级大文件分析,可快速定位内存泄漏疑点、查看对象支配树、引用链、类加载器信息,是堆OOM分析的首选工具。
-
JVisualVM:JDK自带的可视化监控工具,支持实时监控JVM内存、CPU、线程状态,生成并分析堆dump,适合本地开发与测试环境使用。
-
JProfiler:商业级性能分析工具,支持实时内存监控、CPU分析、线程分析、堆dump深度分析,功能强大,适合复杂场景的深度排查。
3.5 第五步:定位根因与问题代码位置
以最常见的堆OOM为例,使用MAT分析的核心步骤如下:
-
打开堆dump文件,选择
Leak Suspects Report(内存泄漏疑点报告),MAT会自动分析出占用内存最大的对象与可疑的内存泄漏点。 -
查看
Dominator Tree(支配树),按照对象占用内存大小排序,找到占用内存最大的对象,查看其所属的类、包名,定位到业务代码中的对象。 -
查看对象的
Path to GC Roots(到GC根节点的引用链),找到是谁持有了该对象的引用,导致其无法被GC回收,这就是内存泄漏的核心根因。 -
结合业务代码,找到引用对象的代码位置,确认泄漏的原因。
3.6 第六步:修复代码并压测验证
定位到根因后,针对性修复代码,修复完成后必须进行压测验证,模拟生产环境的流量与数据量,监控JVM内存变化、GC情况,确认OOM问题彻底解决,无内存泄漏。
四、生产OOM实战案例解析
4.1 案例一:SpringBoot服务堆内存泄漏OOM
现象
线上SpringBoot服务运行3天后,频繁出现Full GC,最终抛出java.lang.OutOfMemoryError: Java heap space异常,服务不可用。
排查过程
-
查看异常类型,锁定为Java堆OOM,获取自动生成的堆dump文件。
-
使用MAT打开dump文件,泄漏疑点报告显示,静态
List<UserInfo>对象占用了90%的堆内存,对象数量超过100万。 -
查看引用链,发现该List是业务代码中的静态缓存集合,用于存储用户登录信息,代码中只有add操作,没有过期删除机制,导致用户对象不断累积,无法被GC回收,最终占满堆内存。
根因
使用静态集合做缓存,没有设置过期时间与最大容量,导致内存泄漏。
修复方案
替换静态集合,使用Guava Cache实现缓存,设置最大容量与过期时间,代码如下:
package com.jam.demo.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.jam.demo.entity.UserInfo;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 用户信息缓存
* @author ken
* @date 2026-03-11
*/
@Component
public class UserInfoCache {
/**
* 缓存最大容量10000,过期时间30分钟
*/
private final Cache<Long, UserInfo> userCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
/**
* 添加用户缓存
* @param userId 用户ID
* @param userInfo 用户信息
*/
public void putUser(Long userId, UserInfo userInfo) {
userCache.put(userId, userInfo);
}
/**
* 获取用户缓存
* @param userId 用户ID
* @return 用户信息
*/
public UserInfo getUser(Long userId) {
return userCache.getIfPresent(userId);
}
/**
* 删除用户缓存
* @param userId 用户ID
*/
public void removeUser(Long userId) {
userCache.invalidate(userId);
}
}
验证
修复后进行压测,连续运行7天,堆内存稳定,无频繁Full GC,OOM问题彻底解决。
4.2 案例二:动态代理导致的元空间OOM
现象
线上服务每次调用接口,元空间内存就上涨1MB左右,运行1天后抛出java.lang.OutOfMemoryError: Metaspace异常。
排查过程
-
查看异常类型,锁定为元空间OOM,查看GC日志,元空间使用率持续上涨,Full GC无法回收。
-
使用jhsdb查看类加载情况,发现服务运行期间生成了超过10万个动态代理类,类名格式为
OOMTargetClass$$EnhancerByCGLIB$$xxx,且每个类的类加载器都不同,无法被卸载。 -
查看业务代码,发现每次接口调用都会新建一个Enhancer对象,重新生成代理类,没有缓存代理类,导致每次调用都生成新的类,元空间持续占满。
根因
动态代理类重复生成,没有缓存,导致类加载器泄漏,元空间无法回收。
修复方案
缓存代理类,避免重复生成,仅在首次调用时生成代理类,后续复用。
验证
修复后,接口调用不再生成新的代理类,元空间内存稳定,Full GC可正常回收无用类,OOM问题彻底解决。
五、生产最佳实践与避坑指南
5.1 JVM参数最佳实践(JDK 17)
-
堆内存设置:-Xms与-Xmx设置为相同值,避免堆动态扩展,生产环境建议设置为物理内存的70%以内,预留足够的系统内存与元空间、直接内存。
-
元空间设置:-XX:MetaspaceSize与-XX:MaxMetaspaceSize设置为相同值,常规业务设置为256m即可,动态生成类较多的场景设置为512m-1g。
-
OOM自动dump :必须配置
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvm/heapdump_${PID}.hprof,保证OOM时自动保留现场。 -
GC日志开启 :必须配置GC日志,JDK 17推荐参数:
-Xlog:gc*:file=/var/log/jvm/gc_%t.log:time,uptime:filecount=10,filesize=100M。 -
直接内存设置:-XX:MaxDirectMemorySize与-Xmx保持一致,IO密集型场景可适当调大。
-
栈内存设置:-Xss默认1m,生产环境建议设置为256k-512k,减少单个线程的内存占用,提升可创建的线程数量。
5.2 OOM预防最佳实践
-
禁止使用静态集合存储大量数据:静态集合的生命周期与JVM一致,不会被GC回收,必须使用缓存框架,设置过期时间与最大容量。
-
ThreadLocal必须手动remove:线程池的线程会复用,若ThreadLocal使用完不手动remove,会导致对象无法被回收,引发内存泄漏,必须在try-finally中执行remove操作。
-
直接内存必须手动释放:Netty的ByteBuf、DirectByteBuffer使用完必须手动释放,避免堆外内存泄漏。
-
动态生成类必须缓存:CGlib、动态代理生成的类必须缓存,避免重复生成,导致元空间泄漏。
-
数据查询必须分页:禁止一次性查询全表数据,避免大对象占满堆内存。
-
定期压测与监控:上线前必须进行压测,监控JVM内存变化、GC情况,提前发现内存泄漏问题;生产环境必须配置JVM监控告警,堆内存使用率超过80%、Full GC频率超过10分钟/次时及时告警。
-
代码评审重点检查:代码评审时重点检查可能导致内存泄漏的代码,如静态集合、ThreadLocal、缓存使用、动态代理等。
5.3 常见避坑指南
-
不要忽略OOM异常的堆栈信息:很多开发者看到OOM就认为是堆内存不足,盲目调大堆内存,忽略了异常类型,导致元空间、直接内存的OOM无法解决,甚至加剧问题。
-
不要在生产环境使用jmap -histo:live命令:该命令会触发Full GC,导致服务停顿,影响线上业务,仅可在测试环境使用。
-
不要使用无限容量的集合:ArrayList、HashMap等集合没有容量限制,若不断添加数据,会导致堆内存持续上涨,最终引发OOM,必须设置最大容量。
-
不要在循环中创建大对象:循环中频繁创建大对象会导致堆内存快速占满,频繁Young GC,甚至Full GC,影响性能,严重时引发OOM。
总结
JVM运行时数据区是Java内存管理的核心,每一块内存区域都有明确的规范边界与OOM触发规则,OOM的本质是内存申请超过了该区域的可用内存上限,要么是配置不合理,要么是代码存在内存泄漏。