JVM 内存区域划分

1. JVM 规范内存区域划分

1.1 总览

根据 Java 虚拟机规范(JVM Specification)的约定,JVM 内存区域划分为以下 5 个部分:

JVM 内存区域(规范层面)

线程私有区域

虚拟机栈
(VM Stack)

本地方法栈
(Native Method Stack)

程序计数器
(PC Register)

线程共享区域

方法区
(Method Area)


(Heap)

  1. 规范 vs 实现:规范定义的是逻辑边界和功能清单,实际实现(如 HotSpot)往往不会严格按照规范来实现
  2. 规范外内存:JVM 实际运行还需要很多规范外的内存,包括:
    • JVM 自身占用
    • GC 占用
    • 线程相关内存(OS 层)
    • 外部资源句柄
    • 堆外内存(DirectByteBuffer)

1.2 方法区

定义:存储类的类型信息(Type Information),即描述类本身的信息。

规范定义的存储内容

根据 JVM 规范,方法区存储:

类型说明示例
类信息类的基本信息类名、父类、接口、访问修饰符
字段信息类的字段定义字段名、字段类型、访问修饰符
方法信息类的方法定义方法字节码、方法签名、异常表
常量池类的常量池引用符号引用、字面量
特点
  • 线程共享:所有线程共享同一个方法区
  • 可抛出 OOM:当加载的类过多时,可能抛出 OutOfMemoryError
  • GC 回收:规范要求支持垃圾回收,但条件苛刻

注意:

  • 规范没有规定方法区的物理位置(可以在堆中,也可以在本地内存)
  • 规范没有规定是否包含 JIT 编译后的代码(这是实现细节)
  • 规范没有规定静态变量和字符串常量池的位置(这是实现细节)

1.3 堆

定义:存储对象实例和数组的运行时数据区域。

规范定义

根据 JVM 规范:

The heap stores objects and arrays.

即:堆只存储对象实例和数组。

特点
  • 线程共享:所有线程共享同一个堆
  • GC 主要区域:垃圾收集器管理的主要区域
  • 可抛出 OOM:当对象过多无法分配时,抛出 OutOfMemoryError: Java heap space
  • 最大内存区域:通常占 JVM 内存的最大部分

注意:

  • 规范只规定了堆存储对象和数组
  • 规范没有规定字符串常量池、静态变量的位置(这是实现细节)
  • 规范没有规定堆的内存结构(新生代、老年代等,这是 GC 实现细节)

1.4 虚拟机栈

定义:描述 Java 方法执行的内存模型,每个方法调用对应一个栈帧的入栈和出栈。

规范定义

根据 JVM 规范:

  • 每个线程都有独立的虚拟机栈
  • 虚拟机栈的生命周期与线程相同
  • 栈帧(Stack Frame)用于存储方法执行的相关数据
栈帧结构(规范定义)

JVM 规范定义了栈帧的四个组成部分:

栈帧 (Stack Frame)

1.局部变量表
基本类型 int, boolean
对象引用 reference
returnAddress

2.操作数栈
方法执行的中间结果

3.动态连接
常量池引用
运行时常量池

4.返回地址
方法正常返回地址
方法异常返回地址

注意
规范定义了栈帧的逻辑结构
规范没有规定栈的物理实现方式

局部变量表详解
类型说明示例
基本类型boolean, byte, char, short, int, long, float, doubleint a = 10
对象引用指向堆中对象的引用String s = "hello"
returnAddress方法返回地址(字节码级别)return 指令
异常情况
异常触发条件
StackOverflowError线程请求的栈深度大于 JVM 允许的深度(如无限递归)
OutOfMemoryError无法分配足够的内存创建新线程

1.5 本地方法栈

定义:为 Native 方法(使用 native 关键字修饰的方法)服务的内存区域。

特点
  • 功能类似虚拟机栈:也是栈帧结构
  • 执行 Native 方法:调用 Java 以外的语言(如 C/C++)编写的方法
  • 依赖 JVM 实现:某些 JVM(如 HotSpot)将虚拟机栈和本地方法栈合并

1.6 程序计数器

定义:记录当前线程所执行的字节码的行号指示器。

特点
  • 线程私有:每个线程都有独立的程序计数器
  • 指向字节码地址:记录当前执行到哪一条字节码指令
  • 控制程序流程:相当于图遍历中的 next 指针
  • 唯一不会 OOM 的区域:JVM 规范中唯一规定不会发生 OutOfMemoryError 的区域
作用
场景说明
顺序执行指向下一条字节码指令
方法调用保存返回地址(调用点)
方法返回恢复到调用点继续执行
异常跳转跳转到异常处理器
特殊情况
  • 执行 Native 方法:程序计数器值为 Undefined(因为 Native 方法不是字节码)

2. HotSpot 实现

注意:以下内容针对 HotSpot JVM(最常用的 JVM 实现),其他 JVM 实现可能不同。

2.1 元空间

定义:HotSpot 对方法区的不完整的实现(JDK 8+)。

与永久代的区别
特性永久代(JDK 7 及之前)元空间(JDK 8+)
内存位置JVM 进程内存(非堆,但由 GC 管理)本地内存(Native Memory)
GC 逻辑与堆共用独立的 GC 逻辑
容量限制固定大小默认无限制(受物理内存限制)
调整参数-XX:PermSize-XX:MaxPermSize-XX:MetaspaceSize-XX:MaxMetaspaceSize
OOM 影响Full GC 可能无休止GC 效率更高
为什么废弃永久代?

历史原因:JDK 6 → 7 的升级已经将类的静态变量引用和字符串字面值移动到了堆中,永久代的问题已部分缓解。

但永久代与堆共用 GC 逻辑仍导致两个严重问题:

问题 1:堆满触发 GC,扫描永久代效率低

堆满了

触发 Full GC

扫描堆 + 永久代

永久代引用复杂
且生命周期长

可能忙了很久
但什么都没收集到

问题 2:永久代满触发 GC,无休止直到 OOM

永久代满了

触发 Full GC

扫描堆 + 永久代

即使堆很空也要扫描

永久代 Full GC
基本回收不到空间

GC 起来没完直到 OOM

元空间的优化

独立的 GC 逻辑

  • 元空间有专门的 GC 回收策略
  • 不再与堆互相影响
  • GC 效率显著提升

容量自动扩展

  • 默认最大无限制(受物理内存限制)
  • 可设置 -XX:MaxMetaspaceSize 防止过度占用

类加载器隔离

  • 以类加载器为单位分配内存
  • 类加载器卸载时一次性回收对应内存

2.2 堆

定义:HotSpot 的堆是 JVM 独享且全权管辖的区域,实际上成为了 JVM 的通用对象容器

存储内容扩展
类型说明为何在堆中
对象实例用户定义的对象规范要求
字符串常量池字符串字面量JDK 7+ 从永久代迁移
静态变量类的静态变量JDK 7+ 从永久代迁移
虚拟线程栈帧虚拟线程的栈帧利用 GC 简化内存管理

设计思想:将字符串常量池和静态变量移入堆,是为了复用 GC 机制和简化 GC 流程。

堆的内存结构

堆的内存结构与 GC 算法强相关,因此此处不作具体介绍。


2.3 栈

定义:HotSpot 对虚拟机栈本地方法栈的实现。

统一实现

HotSpot 中,虚拟机栈和本地方法栈使用同一套实现。

普通线程的栈实现
特性说明
栈类型物理栈(操作系统栈)
内存占用通常为 1MB(可通过 -Xss 调整)
分配位置操作系统本地内存
生命周期与线程相同
虚拟线程的栈实现(JDK 21+)

HotSpot 为虚拟线程引入了栈帧拷贝机制:

特性普通线程虚拟线程
栈类型物理栈栈帧拷贝
运行时操作系统栈物理栈
挂起时仍在操作系统栈冻结到堆内存
内存占用约 1MB几 KB
上下文切换昂贵(OS 级别)便宜(JVM 级别)
数量级数千个数百万个
栈帧拷贝机制详解

虚拟线程启动

阻塞IO发生

持续状态

IO操作完成

继续执行

栈帧在物理栈

栈帧冻结并拷贝到堆
链表伪装栈

等待IO完成

从堆拷贝回物理栈

物理栈
高效执行

堆内存
轻量级挂起

设计优势:

  • 轻量级:栈帧可以冻结在堆中,不占用操作系统栈
  • 高效:挂起/恢复只需要内存拷贝,不需要 OS 上下文切换
  • 可扩展:可以创建数百万个虚拟线程
JIT 优化:逃逸分析与标量替换

逃逸分析:分析对象的作用域,判断对象是否"逃逸"出方法。

标量替换:如果对象未逃逸,可能被拆解成若干个基础变量,直接分配到栈的局部变量表,而不在堆中创建对象

// 示例代码
public void method() {
    Point p = new Point(10, 20);  // 未逃逸
    int x = p.x;
    int y = p.y;
    // JIT 优化后可能变成:
    // int x = 10;
    // int y = 20;
    // 不再创建 Point 对象!
}

优势:

  • 减轻 GC 压力
  • 提高访问速度(栈访问比堆快)
  • 提高缓存命中率

2.4 程序计数器

定义:HotSpot 对程序计数器的实现。

存储位置
线程类型程序计数器位置说明
普通线程CPU 寄存器利用硬件加速
虚拟线程寄存器(运行时)→ 堆(挂起时)挂起时冻结到堆
虚拟线程的程序计数器

虚拟线程启动

IO操作挂起

持续状态

IO完成

继续执行

PC在CPU寄存器

PC冻结到堆内存

等待IO

PC回填到CPU寄存器

CPU寄存器
硬件加速

堆内存
上下文冻结

优势:虚拟线程挂起时,整个上下文(包括程序计数器)都冻结在堆中,可以高效地恢复执行。


3. 规范 vs 实现对比

3.1 关键区别总结

方面JVM 规范HotSpot 实现
定义方式逻辑边界、功能清单具体的物理实现
约束力所有 JVM 必须遵守HotSpot 特有的选择
灵活性留有实现空间可以优化和创新

3.2 各区域的规范 vs 实现

方法区
维度规范要求HotSpot 实现
存储内容类的类型信息元空间(Metaspace)
物理位置未规定本地内存(JDK 8+)
容量限制未规定可设置 MaxMetaspaceSize
字符串常量池未规定位置在堆中(JDK 7+)
静态变量未规定位置在堆中(JDK 7+)
JIT 代码未规定可能包含在元空间
维度规范要求HotSpot 实现
存储内容对象和数组对象、数组、字符串常量池、静态变量、虚拟线程栈帧
内存结构未规定GC 相关
GC 算法要求有 GCSerial/Parallel/G1/ZGC 等
虚拟机栈
维度规范要求HotSpot 实现
栈帧结构规定了 4 个部分完全遵循
物理实现未规定物理栈(普通线程)
虚拟线程未规定栈帧拷贝(JDK 21+)
本地方法栈
维度规范要求HotSpot 实现
功能服务 Native 方法与虚拟机栈合并实现
程序计数器
维度规范要求HotSpot 实现
功能记录字节码地址CPU 寄存器(普通线程)
虚拟线程未规定挂起时冻结到堆

4. 内存区域对比

4.1 线程共享 vs 线程私有

维度线程共享区域线程私有区域
包含方法区、堆虚拟机栈、本地方法栈、程序计数器
生命周期从 JVM 启动到关闭与线程相同
访问冲突需要同步控制无冲突
OOM 风险是(容易发生)是(栈深度超限)
GC 回收主要区域不是主要区域

5. 常见误区

误区 1:混淆规范与实现

错误理解:认为 JVM 规范描述的所有内容(如字符串常量池、静态变量在堆中)都是所有 JVM 必须遵守的。

正确理解

  • JVM 规范只定义了逻辑边界和功能要求
  • 具体实现(如字符串常量池的位置)由各 JVM 自己决定
  • 不同 JVM 实现可能有不同的内存布局

误区 2:方法区和堆是分开的

错误理解:方法区和堆是两个完全独立的区域,互不影响。

正确理解

  • JDK 7 及之前:永久代(方法区实现)虽然是堆外内存,但是和堆共用 GC
  • JDK 8+:元空间(方法区实现)在本地内存,与堆分离
  • 但字符串常量池和静态变量在 JDK 7+ 已移入堆

误区 3:局部变量在栈中,对象在堆中

错误理解:所有局部变量都在栈中,所有对象都在堆中。

正确理解

  • 基本类型局部变量:栈
  • 对象引用:栈(引用本身)→ 堆(实际对象)
  • 未逃逸对象:可能被 JIT 优化到栈(标量替换)
  • 虚拟线程栈帧:堆(挂起时)

误区 4:程序计数器指向 Java 代码行号

错误理解:程序计数器记录的是 Java 源代码的行号。

正确理解

  • 程序计数器指向的是字节码的地址,不是 Java 源代码行号
  • 字节码经过 JIT 编译后变成机器码,程序计数器指向机器码地址
  • 调试器可以通过字节码映射表反查到源代码行号

误区 5:虚拟机栈越大越好

错误理解:增加栈大小可以避免 StackOverflowError

正确理解

  • 增加栈大小确实可以减少 StackOverflowError 的风险
  • 但每个线程占用的内存会增加
  • 可创建的线程数量会减少
  • 权衡:根据实际递归深度和并发需求调整

6. 总结

理解 JVM 内存区域的关键在于严格区分规范定义和具体实现,例如:

方面JVM 规范HotSpot 实现
方法区定义为存储类类型信息的逻辑区域元空间(本地内存)
定义为存储对象和数组的区域对象+数组+字符串常量池+静态变量
定义栈帧的逻辑结构物理栈(普通线程)
栈帧拷贝(虚拟线程)

关键理解:

  • 规范只定义"是什么"(what)
  • 实现决定"怎么做"(how)
  • 不同 JVM 实现可以有不同的实现方式
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐