Java

JVM 内存结构

·12 分钟阅读·4658 字

JVM 运行时数据区详解——线程私有/共享区域、堆内存、方法区、直接内存

📋 目录

JVM 内存结构

面向运维+面试的 JVM 运行时数据区详解,涵盖五大区域作用、配置参数、溢出异常、排查要点。 面向运维+面试,区分线程私有/线程共享、区域作用、溢出异常、运维排查要点,不深挖源码,侧重实际现象与问题定位。

一、整体总览

Java 程序运行时,JVM 会划分出5大运行时数据区,依据「是否随线程创建/销毁」分为两大类:

  1. 线程私有区域:每个线程独有,线程创建时分配,线程销毁时回收,线程间互不干扰
  • 程序计数器
  • Java 虚拟机栈
  • 本地方法栈
  1. 线程共享区域:整个 JVM 实例共用,随 JVM 启动创建、关闭销毁
  • Java 堆
  • 方法区(JDK8 后改为元空间)

补充:直接内存(堆外内存)不属于 JVM 运行时数据区,但运维高频接触,单独讲解。


二、线程私有区域

1. 程序计数器(Program Counter Register)

1.1 作用

  • 记录当前线程正在执行的字节码行号
  • Java 是多线程,CPU 切换线程执行时,靠此区域记录中断位置,线程恢复后继续执行。
  • 如果执行 Native 本地方法,计数器值为 undefined

1.2 核心特点

  • 唯一不会抛出 OutOfMemoryError(OOM) 的区域:内存占用极小、固定大小。
  • 生命周期:跟随线程,线程销毁即释放。

运维关注点

日常故障几乎不会涉及,了解概念即可。


2. Java 虚拟机栈(Java Virtual Machine Stack)

2.1 核心结构

每个线程对应一个虚拟机栈,栈中存储栈帧(Stack Frame)
每调用一个 Java 方法,就创建一个栈帧;方法执行完毕,栈帧出栈销毁。

单个栈帧包含:

  • 局部变量表:存放方法内局部变量、方法参数(基本类型、对象引用地址)
  • 操作数栈:字节码执行时的临时数据栈,做计算、传参
  • 动态链接、方法返回地址、附加信息:底层逻辑,运维无需深究

2.2 内存配置

启动参数:-Xss,设置单个线程栈的大小
例:-Xss256k 表示每个线程栈容量为 256KB。

2.3 两类异常(运维高频)

  1. StackOverflowError 栈溢出
  • 触发原因:

  • 方法无限递归(最典型);

  • 单个线程栈 -Xss 设置过小;

  • 方法调用层级过深。

  • 现象:程序直接抛出栈溢出错误,线程挂掉。

  1. OutOfMemoryError: Unable to create new native thread
  • 触发原因:
  1. 单个线程栈 -Xss 太大,有限内存下,能创建的线程总数变少;
  2. 应用线程数持续暴涨(连接不释放、线程泄漏);
  3. 操作系统用户进程/线程数上限被打满。
  • 场景:服务假死、无法接收新请求、创建线程失败。

运维排查

  • 栈溢出:jstack 查看线程栈,定位无限递归/深层调用的方法;
  • 无法创建线程:先查线程总数、-Xss 配置,再查系统线程限制。

3. 本地方法栈(Native Method Stack)

3.1 作用

功能和Java 虚拟机栈完全一致,区别只在于:

  • 虚拟机栈:执行 Java 代码
  • 本地方法栈:执行 Native 本地方法(由 C/C++ 实现,如 System.currentTimeMillis()、文件、网络、系统调用)。

3.2 异常与配置

  • 同样会抛出 StackOverflowError 和 OOM;
  • JVM 一般不单独提供参数配置,大小跟随虚拟机栈。

运维关注点

极少单独出问题,和虚拟机栈问题统一排查即可。


三、线程共享区域(运维重点,80% 内存故障出自这里)

1. Java 堆(Heap)

1.1 作用

JVM 最大的内存区域,所有对象实例、数组都在堆中分配内存。
是 GC(垃圾回收)的主要工作区域。

1.2 内存配置(生产必配参数)

-Xms :堆初始内存
-Xmx :堆最大内存

✅ 生产规范:-Xms = -Xmx,避免运行时堆动态扩容/缩容带来性能抖动。

1.3 堆分代模型(GC 核心,必学)

现代 JVM 把堆划分为 新生代 + 老年代,分代目的:针对不同生命周期对象,使用不同回收算法,提升 GC 效率

(1)新生代(Young Generation)

存放生命周期短、朝生夕灭的对象(临时对象、局部对象),GC 频率极高。
内部再划分 3 块:

  • Eden(伊甸区):新对象优先分配在这里,占新生代 8/10
  • Survivor 幸存区:分为 From(S0) / To(S1),各占 1/10

规则:同一时间永远只有一块 Survivor 在用,另一块为空

新生代比例参数:
-XX:SurvivorRatio=8 → Eden:S0:S1 = 8:1:1(默认值)

(2)老年代(Old Generation / Tenured)

存放存活时间久、体积大的对象:

  1. 新生代多次 GC 后依然存活的对象;
  2. 超过阈值的大对象(直接进入老年代,避免新生代复制开销);
  3. 动态晋升的对象。

GC 频率低、回收速度慢,一旦触发 Full GC 会造成明显业务卡顿。

对象流转完整流程
  1. 新对象 → Eden 区;
  2. Eden 区满 → 触发 Minor GC(新生代GC)
  3. 存活对象移入空闲的 Survivor 区,对象年龄 +1;
  4. 对象年龄达到阈值(默认15)→ 晋升老年代;
  5. 老年代空间不足 → 触发 Major GC / Full GC

1.4 堆内存溢出:java.lang.OutOfMemoryError: Java heap space

触发场景
  1. 内存泄漏:对象被长生命周期引用持有,无法被 GC 回收,内存缓慢上涨,最终占满堆;
  2. 内存溢出(非泄漏):瞬时并发、批量数据查询,创建大量临时对象,堆容量不足;
  3. -Xmx 设置过小,无法承载业务峰值。
运维排查手段
  1. jstat -gc 观察 GC 频率、堆使用率;
  2. 开启 GC 日志,分析回收效果;
  3. jmap 生成堆快照,用 MAT 分析大对象、泄漏对象。

2. 方法区(Method Area)& 元空间(Metaspace)

2.1 演进历史(关键考点)

  • JDK 1.6 及之前:永久代(PermGen),属于堆的一部分,受 -Xmx 限制;
  • JDK 1.7:逐步剥离,字符串常量池、静态变量移出永久代;
  • JDK 8 及以后彻底移除永久代,改用元空间 Metaspace

元空间直接使用宿主机本地物理内存,不再占用堆内存。

2.2 存储内容(JDK8 元空间)

存储类的元数据(描述类本身的信息):

  1. 加载的类、接口、注解、枚举信息;
  2. 方法信息、字段信息、运行时常量池;
  3. 动态生成的类(CGLIB 动态代理、ASM、热部署、反射、Groovy 等)。

2.3 核心配置参数(JDK8+)

-XX:MetaspaceSize      元空间初始大小
-XX:MaxMetaspaceSize   元空间最大上限(建议手动设置,防止无限占用系统内存)

2.4 元空间溢出:java.lang.OutOfMemoryError: Metaspace

典型触发场景(运维高频)
  1. 应用热部署(反复加载/卸载类);
  2. 大量使用动态代理、反射、字节码增强;
  3. 框架、中间件批量动态生成类;
  4. MaxMetaspaceSize 设置过小。
现象与排查
  • 现象:服务启动失败、运行中突然崩溃,日志报 Metaspace OOM;
  • 排查:查看已加载类数量、动态类生成逻辑,调整元空间上限。

四、补充:直接内存(堆外内存 Direct Memory)

不属于 JVM 运行时数据区,但运维、中间件(Netty、NIO)必接触。

4.1 作用

  • NIO、Netty 等框架使用 DirectByteBuffer 分配堆外内存;
  • 读写文件、网络数据时,避免堆内存与系统内存拷贝,提升IO性能。

4.2 特点

  1. 不受 -Xmx 堆大小限制,使用操作系统物理内存
  2. 回收依赖系统 GC 或手动释放,JVM GC 无法直接管理;

4.3 溢出异常

OutOfMemoryError: Direct buffer memory

触发原因

  1. 频繁创建堆外缓冲区但不主动释放;
  2. 堆外内存使用无限制,占满系统内存;
  3. Netty/NIO 组件使用不当、连接泄漏。

4.4 配置参数

-XX:MaxDirectMemorySize:限制最大堆外内存。


五、区域汇总对比表(背诵版)

内存区域归属主要存储内容常见异常核心配置参数
程序计数器线程私有字节码执行行号无OOM
Java 虚拟机栈线程私有栈帧、局部变量、方法调用StackOverflow、无法创建线程-Xss
本地方法栈线程私有Native 方法栈帧同虚拟机栈无单独参数
Java 堆线程共享对象、数组、分代内存Java heap space OOM-Xms、-Xmx、-XX:SurvivorRatio
元空间(JDK8+)线程共享类元数据、常量池Metaspace OOM-XX:MetaspaceSize、MaxMetaspaceSize
直接内存堆外NIO/Netty 缓冲区Direct buffer OOM-XX:MaxDirectMemorySize

六、运维核心总结 & 排查思路

  1. 栈相关问题:优先看 jstack,定位递归、线程泄漏、线程数打满;
  2. 堆相关问题:线上第一看 jstat + GC 日志,OOM 后用 jmap+MAT 分析堆快照,区分「内存泄漏」和「容量不足」;
  3. 元空间问题:重点排查热部署、动态代理、字节码框架,合理设置元空间上限;
  4. 堆外内存:Netty、NIO 应用专属,检查连接/缓冲区是否泄漏,限制最大堆外内存;
  5. 容器环境额外注意:Docker 中 JVM 要开启 CGroup 感知,避免 JVM 无视容器内存限制被系统 OOM Kill。

七、面试高频问答(运维岗)

  1. JVM 运行时数据区分为哪几块?哪些线程私有、哪些共享?
    答:线程私有:程序计数器、虚拟机栈、本地方法栈;线程共享:堆、元空间。程序计数器是唯一无OOM的区域。
  2. 虚拟机栈和堆的区别?
    答:栈存方法调用、局部变量,线程私有,生命周期短;堆存对象实例,线程共享,GC 主要区域,容易发生内存溢出。
  3. JDK8 为什么移除永久代改用元空间?
    答:永久代大小难预估、易OOM;元空间使用系统物理内存,弹性更好,同时简化 JVM 实现。
  4. 堆内存溢出和元空间溢出分别是什么原因?
    答:堆溢出多为内存泄漏或堆大小不足;元空间溢出多为热部署、动态生成类、元空间配置过小。

关联文档