
这两个概念在 Java 开发中经常被混淆,但它们描述的是完全不同的维度:
JVM 运行时数据区:描述的是数据存储的物理/逻辑结构(即"东西放在哪")。
JMM:描述的是多线程并发下的访问规则(即"线程怎么读写")。
Java 内存体系深度指南:运行时数据区与 JMM
第一部分:JVM 运行时数据区 (Runtime Data Areas)
------ 关注数据的存储与生命周期
JVM 在运行 Java 程序时,会将其管理的内存划分为若干个不同的数据区域。这些区域有各自的用途、创建和销毁时间。
- 线程私有区域 (Thread-Local)
这些区域随线程创建而创建,随线程结束而销毁,不需要进行垃圾回收,且无线程安全问题。
1.1 程序计数器 (Program Counter Register)
核心作用:当前线程所执行的字节码的行号指示器。
深层细节:
这是 JVM 规范中唯一一个没有规定 OutOfMemoryError 的区域。
如果执行的是 Java 方法,记录的是指令地址;如果执行的是 Native 方法,计数器值为 Undefined。
作用场景:多线程切换后,线程需要知道上次执行到哪里了,就靠它恢复。
1.2 Java 虚拟机栈 (Java Virtual Machine Stacks)
核心作用:描述 Java 方法执行的内存模型。
栈帧 (Stack Frame):每个方法执行时都会创建一个栈帧,包含以下内容:
局部变量表 (Local Variable Table):存放编译期可知的基本数据类型、对象引用。以 Slot (槽) 为单位,double 和 long 占两个 Slot。
操作数栈 (Operand Stack):后入先出栈,用于计算过程中的临时存储(如 iadd 指令就是从这里弹出两个数相加)。
动态链接:指向运行时常量池的方法引用(支持多态)。
方法返回地址:方法正常或异常退出的定义。
异常:
StackOverflowError:递归过深,栈深度超过限制。
OutOfMemoryError:无法申请到足够的内存来扩展栈。
1.3 本地方法栈 (Native Method Stack)
与虚拟机栈类似,区别在于它是为 Native 方法(JNI,如 C++ 编写的底层库)服务的。
- 线程共享区域 (Thread-Shared)
这些区域是垃圾回收(GC)的重点战场,存在线程安全问题。
2.1 Java 堆 (Java Heap)
核心作用:存放对象实例和数组。是 JVM 管理的最大一块内存。
内存划分 (经典分代):
新生代 (Young Gen):Eden + S0 + S1。对象在这里诞生,朝生夕死。
老年代 (Old Gen):存放生命周期长的对象。
TLAB (Thread Local Allocation Buffer):
深度机制:为了避免多线程在堆上分配对象时的锁竞争,JVM 默认在 Eden 区为每个线程分配一块私有的缓存区域(TLAB)。
意义:线程在自己的 TLAB 上分配对象不需要加锁,效率极高(指针碰撞)。只有 TLAB 用完时,才需要同步锁定申请新的 TLAB。
2.2 方法区 (Method Area)
核心作用:存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码。
演进历史:
JDK 7 及之前:实现为 永久代 (PermGen),位于堆内存中(逻辑上)。容易 OOM。
JDK 8 及之后:实现为 元空间 (Metaspace),使用 本地内存 (Native Memory)。
为什么变? 字符串常量池移入堆中,减少 PermGen OOM 风险;Native Memory 只要物理内存够大就不会溢出。
2.3 运行时常量池 (Runtime Constant Pool)
是方法区的一部分。Class 文件中的常量池表(字面量和符号引用)在类加载后存放到这里。具备动态性(如 String.intern() 可以动态加入常量)。
第二部分:Java 内存模型 (Java Memory Model, JMM)
------ 关注多线程的原子性、可见性与有序性
JMM 是一种抽象规范(JSR-133),它屏蔽了底层硬件(寄存器、缓存、内存)和操作系统(编译器优化、处理器重排序)的差异,保证 Java 程序在各种平台下对内存的访问都能达到一致的效果。
- JMM 的抽象架构
JMM 定义了线程和主内存之间的抽象关系:
主内存 (Main Memory):
所有变量(共享变量)都存在这里。
对应物理硬件的 RAM(内存条)。
工作内存 (Working Memory):
每个线程私有。
保存了该线程使用到的变量的主内存副本。
对应物理硬件:寄存器 + L1/L2/L3 缓存。
交互规则:线程不能直接读写主内存,必须先在工作内存中操作,再同步回主内存。
- JMM 解决的三大核心问题
2.1 可见性 (Visibility)
问题:线程 A 修改了变量,线程 B 无法立即看到,因为线程 A 改的是自己缓存里的副本。
解决方案:
volatile:保证修改后立即刷新到主内存,并使其他线程的缓存失效(基于 MESI 缓存一致性协议和嗅探机制)。
synchronized / Lock:解锁前必须把变量同步回主内存。
2.2 原子性 (Atomicity)
问题:i++ 操作看起来是一行代码,实际包含"读-改-写"三个步骤。多线程下会被打断。
解决方案:
synchronized / Lock:保证同一时刻只有一个线程执行。
CAS (Compare And Swap):JUC 包下的原子类(如 AtomicInteger)使用的无锁机制。
2.3 有序性 (Ordering)
问题:为了优化性能,编译器和处理器会对指令进行重排序 (Reordering)。
示例:a=1; b=2; 可能被重排为 b=2; a=1;。单线程下没问题,但多线程下(如双重检查锁单例)会导致逻辑错误。
解决方案:
volatile:通过插入内存屏障 (Memory Barrier) 禁止特定类型的重排序。
Happens-Before 原则:JMM 定义的一系列天然的先行发生关系(如:启动线程的操作先于线程内的任何操作)。
第三部分:深度对比与关联
这是最容易混淆的部分,请仔细阅读。
- 两个"栈"的区别
JVM 虚拟机栈:是数据结构。里面存的是栈帧,栈帧里有局部变量表。
JMM 工作内存:是逻辑概念。它涵盖了 CPU 寄存器、L1/L2 缓存等。
关联:通常情况下,JVM 栈中的局部变量(非共享)主要存在于 JMM 的工作内存(寄存器/缓存)中;而堆中的对象(共享)主要存在于主内存中,但也会被加载到工作内存(缓存)中。 - 两个"内存"的映射
JMM 概念 对应的 JVM 区域 对应的物理硬件
主内存 Java 堆 (Heap)、方法区 物理内存 (RAM)
工作内存 虚拟机栈 (部分)、程序计数器 CPU 寄存器、L1/L2/L3 缓存 - 为什么需要两套模型?
JVM 运行时数据区是为了管理内存的生命周期(分配、回收、结构化存储)。
JMM 是为了解决并发编程的安全性(缓存一致性、指令重排序)。
第四部分:总结与面试核心
核心口诀
JVM 内存分五块:堆、栈(Java栈/本地栈)、方法区、程序计数器。
JMM 讲三点:原子性、可见性、有序性。
堆是存对象的,栈是运行方法的。
volatile 管可见和有序,synchronized 全都管。
常见误区纠正
误区:"栈是线程安全的,所以栈里的变量都在工作内存;堆是线程共享的,所以堆里的变量都在主内存。"
纠正:不完全正确。JMM 中,堆中的对象副本也会进入工作内存(CPU 缓存)。如果 CPU 缓存不一致,堆里的对象也会出现线程安全问题。这就是为什么堆上的变量也需要 volatile 或锁来保证可见性。