1.1 JVM 内存区域划分

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, double int 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 优化:逃逸分析与标量替换

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

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

java 复制代码
// 示例代码
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 算法 要求有 GC Serial/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 实现可以有不同的实现方式
相关推荐
没有bug.的程序员2 小时前
Spring Cloud Eureka:注册中心高可用配置与故障转移实战
java·spring·spring cloud·eureka·注册中心
dyyx1113 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python
CryptoRzz3 小时前
如何高效接入日本股市实时数据?StockTV API 对接实战指南
java·python·kafka·区块链·状态模式·百度小程序
尽兴-3 小时前
JVM垃圾收集器深度解析:G1与ZGC
jvm·gc·zgc·g1·垃圾收集器java
qingwufeiyang_5303 小时前
JVM调优实战
jvm
码农水水3 小时前
中国邮政Java面试被问:容器镜像的多阶段构建和优化
java·linux·开发语言·数据库·mysql·面试·php
若鱼19193 小时前
SpringBoot4.0新特性-BeanRegistrar
java·spring
好好研究4 小时前
SpringBoot - yml配置文件
java·spring boot·spring
学海无涯书山有路4 小时前
Android FragmentContainerView 新手详解(Java 版)
android·java·开发语言