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

线程私有区域随线程创建而分配,随线程销毁而回收,不存在线程安全问题,每个区域的职责与异常边界完全独立。
程序计数器是一块极小的内存空间,也是JVM规范中唯一不会抛出任何OutOfMemoryError的内存区域。
Java虚拟机栈是Java方法执行的内存模型,线程私有,生命周期与线程完全一致。
package com.jam.demo.jvm.oom;
import lombok.extern.slf4j.Slf4j;
/**
* StackOverflowError 复现示例
* VM参数:-Xss128k 缩小栈容量,快速复现
* @author ken
* @date 2026-03-11
*/
@Slf4j
publicclass SOEDemo {
privatestaticint stackDepth = 0;
/**
* 无限递归方法,触发栈深度溢出
*/
public static void infiniteRecursion() {
stackDepth++;
infiniteRecursion();
}
public static void main(String[] args) {
try {
infiniteRecursion();
} catch (StackOverflowError e) {
log.error("栈深度溢出,当前栈深度:{}", stackDepth, e);
}
}
}
本地方法栈与Java虚拟机栈的职责、结构完全一致,核心区别在于:Java虚拟机栈为Java方法(字节码)服务,而本地方法栈为Native本地方法服务。
线程共享区域随JVM启动而创建,随JVM退出而销毁,所有线程共享该区域的内存,是垃圾回收的核心区域,也是OOM异常的高发区。
Java堆是JVM中最大的一块内存区域,线程共享,JVM启动时创建,唯一核心目的是存储对象实例与数组,《Java虚拟机规范》明确规定:所有的对象实例以及数组都应当在堆上分配。
java.lang.OutOfMemoryError: Java heap space异常,这是生产环境最常见的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
publicclass HeapOOMDemo {
/**
* 1MB大小的字节数组,快速占用堆内存
*/
privatestaticfinalbyte[] ONE_MB_ARRAY = newbyte[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();
}
}
}
}
方法区是线程共享的内存区域,JVM启动时创建,核心用于存储类的元数据信息,《Java虚拟机规范》定义其存储内容包括:运行时常量池、类的字段与方法数据、构造函数与普通方法的字节码内容、注解信息、泛型信息等。
java.lang.OutOfMemoryError: Metaspace异常,核心场景为动态生成大量类、类加载器泄漏、元空间容量设置过小。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
publicclass 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);
}
}
/**
* 被代理的目标类
*/
publicstaticclass OOMTargetClass {
}
}
直接内存不是《Java虚拟机规范》中定义的运行时数据区,也不属于JVM管理的内存,但在Java NIO编程中被频繁使用,也是生产环境OOM的高发区,必须纳入内存管理体系。
java.lang.OutOfMemoryError: Direct buffer memory异常,核心场景为频繁分配大的直接内存、直接内存泄漏、容量设置过小。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
publicclass DirectMemoryOOMDemo {
privatestaticfinalint 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);
}
}
}
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定位的核心是保留完整现场,按照标准化流程逐步排查,避免盲目猜测,以下流程可覆盖99%的生产OOM场景,可直接落地执行。

通过异常堆栈的错误信息,精准匹配上文中的异常类型对照表,快速锁定出问题的内存区域,缩小排查范围,这是定位的核心前提。禁止在未明确异常类型的情况下盲目分析堆dump。
OOM发生后,绝对禁止第一时间重启服务,重启会导致所有现场数据丢失,无法定位根因。必须先收集以下完整数据,再执行重启操作:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvm/heapdump_${PID}.hprof,OOM发生时自动生成dump文件,带上PID避免文件覆盖。若未提前配置,可通过命令jmap -dump:format=b,file=heapdump.hprof <pid>手动生成。-Xlog:gc*:file=/var/log/jvm/gc_%t.log:time,uptime:filecount=10,filesize=100M,实现日志滚动,避免单个文件过大。jstack <pid> > threaddump.txt生成。根据异常类型,直接锁定排查方向,避免无效操作:
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 |
以最常见的堆OOM为例,使用MAT分析的核心步骤如下:
Leak Suspects Report(内存泄漏疑点报告),MAT会自动分析出占用内存最大的对象与可疑的内存泄漏点。Dominator Tree(支配树),按照对象占用内存大小排序,找到占用内存最大的对象,查看其所属的类、包名,定位到业务代码中的对象。Path to GC Roots(到GC根节点的引用链),找到是谁持有了该对象的引用,导致其无法被GC回收,这就是内存泄漏的核心根因。定位到根因后,针对性修复代码,修复完成后必须进行压测验证,模拟生产环境的流量与数据量,监控JVM内存变化、GC情况,确认OOM问题彻底解决,无内存泄漏。
线上SpringBoot服务运行3天后,频繁出现Full GC,最终抛出java.lang.OutOfMemoryError: Java heap space异常,服务不可用。
List<UserInfo>对象占用了90%的堆内存,对象数量超过100万。使用静态集合做缓存,没有设置过期时间与最大容量,导致内存泄漏。
替换静态集合,使用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
publicclass UserInfoCache {
/**
* 缓存最大容量10000,过期时间30分钟
*/
privatefinal 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问题彻底解决。
线上服务每次调用接口,元空间内存就上涨1MB左右,运行1天后抛出java.lang.OutOfMemoryError: Metaspace异常。
xxx,且每个类的类加载器都不同,无法被卸载。
动态代理类重复生成,没有缓存,导致类加载器泄漏,元空间无法回收。
缓存代理类,避免重复生成,仅在首次调用时生成代理类,后续复用。
修复后,接口调用不再生成新的代理类,元空间内存稳定,Full GC可正常回收无用类,OOM问题彻底解决。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvm/heapdump_${PID}.hprof,保证OOM时自动保留现场。-Xlog:gc*:file=/var/log/jvm/gc_%t.log:time,uptime:filecount=10,filesize=100M。JVM运行时数据区是Java内存管理的核心,每一块内存区域都有明确的规范边界与OOM触发规则,OOM的本质是内存申请超过了该区域的可用内存上限,要么是配置不合理,要么是代码存在内存泄漏。