JVM的内存模型和内存结构
理解 JVM 内存模型 (JMM, Java Memory Model) 和 JVM 内存结构 (Runtime Data Areas) 是掌握 Java 程序运行机制和并发编程的关键。这两者紧密相关但关注的层面不同:
一、JVM 内存结构 (Runtime Data Areas) - "物理"层面的内存划分
JVM 内存结构定义了 Java 虚拟机在执行 Java 程序时,操作系统分配给它的一块内存区域是如何被划分成不同功能部分的。这些区域有各自的生命周期和用途。主要包含以下几部分:
-
程序计数器 (Program Counter Register)
- 作用: 当前线程执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖它。
- 特点:
- 线程私有: 每个线程都有自己独立的计数器,互不干扰。
- 占用内存小: 可以看作是线程执行的“书签”。
- 没有
OutOfMemoryError
: 此区域是唯一一个在 JVM 规范中没有规定任何OOM
情况的区域。
-
Java 虚拟机栈 (Java Virtual Machine Stacks)
- 作用: 描述 Java 方法执行的线程内存模型。每个方法被执行时,JVM 会同步创建一个栈帧 (Stack Frame) 用于存储:
- 局部变量表 (
Local Variables
): 存放编译期可知的各种基本数据类型(boolean
,byte
,char
,short
,int
,float
,long
,double
)、对象引用 (reference
类型,不等同于对象本身,可能是指向对象起始地址的指针或句柄) 和returnAddress
类型(指向了一条字节码指令的地址)。 - 操作数栈 (
Operand Stack
): 方法执行过程中进行算术运算或调用其它方法时传递参数的临时工作区。 - 动态链接 (
Dynamic Linking
): 指向运行时常量池中该栈帧所属方法的引用(支持运行时绑定)。 - 方法返回地址 (
Return Address
): 方法正常退出或异常退出时,代码需要返回到的位置。
- 局部变量表 (
- 生命周期: 与线程相同。
- 特点:
- 线程私有。
- 方法调用对应栈帧入栈;方法结束(正常 return 或 抛出异常未被捕获)对应栈帧出栈。
- 错误:
-
StackOverflowError
: 线程请求的栈深度大于虚拟机所允许的深度(通常由无限递归引起)。 -
OutOfMemoryError
: 如果栈可以动态扩展但在扩展时无法申请到足够内存(比较少见)。
-
- 作用: 描述 Java 方法执行的线程内存模型。每个方法被执行时,JVM 会同步创建一个栈帧 (Stack Frame) 用于存储:
-
本地方法栈 (Native Method Stacks)
- 作用: 为 JVM 调用操作系统本地(
native
, 非 Java 编写)方法服务。 - 特点:
- 线程私有。
- 其具体实现由虚拟机自由决定(例如 HotSpot VM 将 Java 虚拟机栈和本地方法栈合二为一)。
- 错误: 同样会抛出
StackOverflowError
和OutOfMemoryError
。
- 作用: 为 JVM 调用操作系统本地(
-
Java 堆 (Java Heap)
- 作用: 存放对象实例和数组。这是垃圾收集器管理的主要区域,因此常被称为 "GC 堆" (Garbage Collected Heap)。
- 特点:
- 所有线程共享。
- 在虚拟机启动时创建。
- 是 JVM 管理的最大一块内存区域。
- 可以处于物理上不连续但逻辑上连续的内存空间中。
- 可进一步划分以提高 GC 效率(如新生代 Eden, S0, S1;老年代)。
- 错误:
OutOfMemoryError
是此区域最常见的错误,当堆中没有足够内存完成实例分配且堆也无法再扩展时抛出。
-
方法区 (Method Area)
- 作用: 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。可以看作是堆的一个逻辑部分,但规范要求与堆区分开管理。
- 具体实现演进:
- 永久代 (Permanent Generation, PermGen) - (<=JDK7): HotSpot VM 早期使用永久代来实现方法区。容易导致
PermGen OOM
。 - 元空间 (Metaspace) - (>= JDK8): 使用本地内存 (Native Memory) 来实现方法区。不再受
-XX:MaxPermSize
限制,默认只受本地内存大小限制。可通过-XX:MaxMetaspaceSize
设置上限。类元数据信息在类加载器存活期间保留,当类加载器不再被引用且类元数据不再被引用时,垃圾收集器会回收这部分内存。字符串常量池移至堆中。
- 永久代 (Permanent Generation, PermGen) - (<=JDK7): HotSpot VM 早期使用永久代来实现方法区。容易导致
- 特点:
- 所有线程共享。
- 可选择不实现垃圾回收或回收效率低。
- 错误:
OutOfMemoryError
,在元空间下,如果类元数据占用超过-XX:MaxMetaspaceSize
(或默认的本地内存限制),就会抛出。
-
运行时常量池 (Runtime Constant Pool)
- 作用: 方法区的一部分。存放编译期生成的各种字面量 (Literal) 和符号引用 (Symbolic References)。
- 特点:
- 具备动态性:运行期间也可能将新的常量放入池中(如
String.intern()
方法)。 - 位置: 在 JDK7 之前属于方法区(永久代);从 JDK7 开始,字符串常量池 (
String Table
) 被移到堆中;JDK8 之后整个运行时常量池随类型信息(Klass)存放在元空间。
- 具备动态性:运行期间也可能将新的常量放入池中(如
- 错误:
OutOfMemoryError
。
-
直接内存 (Direct Memory)
- 作用: 不属于 JVM 运行时数据区,也不是 JVM 规范定义的内存区域。它是通过
java.nio
包下的DirectByteBuffer
对象分配的内存块。这些对象本身在堆上,但它们指向的内存空间是在操作系统分配的本地内存。 - 特点:
- 避免了在 Java 堆和 Native 堆之间来回复制数据(零拷贝),提高性能(如文件 I/O、网络 I/O)。
- 分配回收成本较高,不受 JVM GC 直接管理(回收
DirectByteBuffer
对象时会触发对应的本地内存回收机制)。
- 错误: 虽然不受
-Xmx
等堆参数限制,但受限于操作系统的总内存 (RAM + swap)。如果直接内存分配失败,也会导致 OutOfMemoryError
。
- 作用: 不属于 JVM 运行时数据区,也不是 JVM 规范定义的内存区域。它是通过
图示 (简化版,重点突出堆、栈、方法区/元空间):
+-------------------------------------------------+| JVM Process || +---------------------------------------------+ || | Runtime Data Areas | || | +-------------------+ +-------------------+ | || | | Java Heap | | Metaspace (JDK8+) | | <--- (All Threads Shared)| | | (Objects, Arrays) | | (Class Metadata, | || | | | | Static Vars, etc) | || | +---------^---------+ +---------^---------+ | || | | | | || | +---------+---------+ +---------+---------+ | || | | Method Area | | Runtime Constant | | |--- (JDK7+: String Table in Heap)| | | (Logical Part of | | Pool | | || | | Heap) | | | | || | +-------------------+ +-------------------+ | || | | || | +---------+ +---------+ +---------+ | || | | Thread1 | | Thread2 | | ThreadN | ... | || | +---------+ +---------+ +---------+ | || | | PC | PC | PC | || | | JVM | JVM | JVM | || | | Stack | Stack | Stack | | <--- (Per-Thread Private)| | | (Native | (Native | (Native | || | | Stack) | Stack) | Stack) | || | v v v | || +---------------------------------------------+ || || +---------------------------------------------+ || | Direct Memory | | <--- (Native Memory, Non-JVM Managed)| | (Allocated via NIO DirectByteBuffer) | || +---------------------------------------------+ |+-------------------------------------------------+
二、JVM 内存模型 (JMM) - "并发"层面的规则与约定
JMM 是 Java Memory Model 的缩写。它不是一个物理的内存布局,而是一个抽象的概念、一组规则和规范。它的核心目标是定义在多线程并发访问共享变量(主要在堆上)时,一个线程如何以及何时能够看到另一个线程写入的值,以及如何同步对共享变量的访问。
JMM 要解决的核心问题:
- 可见性 (Visibility): 一个线程修改了共享变量的值,另一个线程能否立即看到这个修改?
- 原子性 (Atomicity): 一个操作(可能涉及多个步骤)是否不可中断?不会出现中间状态?
- 有序性 (Ordering): 程序代码的执行顺序是否就是实际执行的顺序?编译器和处理器为了优化性能会对指令进行重排序,这在单线程下是安全的,但在多线程下可能导致问题。
JMM 的核心概念:
- 主内存 (Main Memory):
- 存储所有共享变量(实例字段、静态字段、构成数组的对象元素)的实际值。
- 可以看作是物理内存的抽象表示。
- 工作内存 (Working Memory):
- 每个线程都有一个私有的工作内存。
- 存储该线程使用到的变量的主内存副本拷贝。
- 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的数据。
- 工作内存是 JMM 的一个抽象概念,它涵盖了寄存器、高速缓存(Cache)、写缓冲区以及其他硬件和编译器的优化。
- 内存间交互操作:
- JMM 定义了 8 种原子操作来完成主内存和工作内存之间的交互:
lock
(锁定) /unlock
(解锁):作用于主内存变量,标记变量为线程独占状态。read
(读取):作用于主内存变量,把变量值从主内存传输到线程的工作内存。load
(载入):作用于工作内存变量,把read
操作得到的值放入工作内存的变量副本中。use
(使用):作用于工作内存变量,把变量值传递给执行引擎。assign
(赋值):作用于工作内存变量,把从执行引擎接收到的值赋给工作内存变量。store
(存储):作用于工作内存变量,把变量值从工作内存传输到主内存。write
(写入):作用于主内存变量,把store
操作得到的值放入主内存的变量中。
- 这些操作必须满足特定的规则(如
read/load
,store/write
必须成对出现且顺序执行等)。
- JMM 定义了 8 种原子操作来完成主内存和工作内存之间的交互:
JMM 的关键特性与解决方案:
- 原子性保障:
- 基本数据类型的读写(除
long
,double
的非volatile
声明外)本身是原子的。 - 更大范围的原子性需要通过
synchronized
块(锁)或java.util.concurrent.atomic
包中的原子类(如AtomicInteger
)来保证。
- 基本数据类型的读写(除
- 可见性保障:
-
volatile
关键字:- 保证变量的可见性:当一个线程修改了
volatile
变量的值,新值会立即刷新到主内存。其他线程读取该变量时,会强制从主内存重新加载最新值。 - 禁止指令重排序优化(部分有序性保障)。
- 保证变量的可见性:当一个线程修改了
-
synchronized
关键字:- 在解锁 (
unlock
) 前,必须把此变量同步回主内存 (store
,write
)。 - 在加锁 (
lock
) 后,会清空工作内存中此变量的值,从而需要重新从主内存read
,load
。 - 保证了同步块内操作的原子性、可见性和有序性。
- 在解锁 (
-
final
关键字: 合理使用final
修饰的字段,在其构造器初始化完成后,对其他线程是可见的(需要正确构造)。
-
- 有序性保障 (Happens-Before 原则):
- JMM 的核心机制是 Happens-Before (先行发生) 原则。它定义了操作之间的偏序关系。如果操作
A
Happens-Before 操作B
,那么A
的结果对B
是可见的(即使A
和B
不在同一个线程)。 - 基本规则 (部分):
- 程序次序规则 (Program Order Rule): 在单线程内,书写在前面的操作 (
A
) Happens-Before 书写在后面的操作 (B
)。(仅针对控制流顺序,依赖数据顺序的指令仍可能重排) - 管程锁定规则 (Monitor Lock Rule): 一个
unlock
操作 Happens-Before 后续(按时间顺序)对同一个锁的lock
操作。 -
volatile
变量规则: 对一个volatile
变量的写操作 Happens-Before 任意后续(按时间顺序)对这个volatile
变量的读操作。 - 线程启动规则 (Thread Start Rule):
Thread.start()
Happens-Before 此线程的任何操作。 - 线程终止规则 (Thread Termination Rule): 线程的所有操作都 Happens-Before 对此线程的终止检测(如
Thread.join()
返回成功、Thread.isAlive()
返回false
)。 - 线程中断规则 (Thread Interruption Rule): 对线程
interrupt()
方法的调用 Happens-Before 被中断线程检测到中断事件 (InterruptedException
,Thread.interrupted()
,Thread.isInterrupted()
)。 - 对象终结规则 (Finalizer Rule): 一个对象的初始化完成(构造方法执行结束) Happens-Before 它的
finalize()
方法的开始。 - 传递性 (Transitivity): 如果
A
Happens-BeforeB
,且B
Happens-BeforeC
,那么A
Happens-BeforeC
。
- 程序次序规则 (Program Order Rule): 在单线程内,书写在前面的操作 (
-
as-if-serial
语义: 单线程下,无论怎么重排序,最终执行结果必须与程序顺序执行的结果一致。Happens-Before
是多线程环境下as-if-serial
的扩展。
- JMM 的核心机制是 Happens-Before (先行发生) 原则。它定义了操作之间的偏序关系。如果操作
总结:内存模型 vs. 内存结构
特性 | JVM 内存结构 (Runtime Data Areas) | JVM 内存模型 (JMM, Java Memory Model) |
---|---|---|
关注点 | 物理/逻辑内存划分:程序运行所需内存区域的划分、用途、生命周期。 | 并发语义:多线程环境下如何正确地、可预测地访问共享内存。 |
核心内容 | 程序计数器、栈 (JVM栈/本地方法栈)、堆、方法区 (元空间)、运行时常量池、直接内存。 | 主内存、工作内存、内存交互操作、volatile , synchronized , final , happens-before 原则。 |
目的 | 组织和管理 JVM 运行时所需的内存区域。 | 定义多线程程序中共享变量访问的规则,解决并发三大问题(原子性、可见性、有序性)。 |
视角 | 从 JVM 内部实现和执行引擎的角度看内存布局。 | 从 Java 程序员和编译器/处理器 的角度看并发访问规则。 |
关系 | JMM 主要规范的是堆和方法区(存储共享变量)在多线程下的访问行为。 volatile 、synchronized 的实现机制往往依赖于内存结构(如锁信息可能存储在对象头中,位于堆上)。 | JMM 定义的规则约束了线程如何与内存结构(尤其是堆)交互以实现正确的并发。 |
简单来说:
- 内存结构 告诉你 JVM 把内存分成了哪几块,每块是干什么用的。
- 内存模型 告诉你,尤其是当多个线程同时运行时,它们应该怎么正确地读写(主要是堆里的)共享数据,避免出现莫名其妙的结果。
理解内存结构有助于诊断内存溢出 (OOM
)、栈溢出 (SOF
) 等问题。理解内存模型 (JMM
) 是编写正确、高效并发程序 (synchronized
, volatile
, Lock
, AtomicXXX
, ConcurrentHashMap
等) 的基础。两者都是深入掌握 Java 和 JVM 不可或缺的知识。