jvm运行时数据区& Java 内存模型

这两个概念在 Java 开发中经常被混淆,但它们描述的是完全不同的维度:

JVM 运行时数据区:描述的是数据存储的物理/逻辑结构(即"东西放在哪")。

JMM:描述的是多线程并发下的访问规则(即"线程怎么读写")。

Java 内存体系深度指南:运行时数据区与 JMM

第一部分:JVM 运行时数据区 (Runtime Data Areas)

------ 关注数据的存储与生命周期

JVM 在运行 Java 程序时,会将其管理的内存划分为若干个不同的数据区域。这些区域有各自的用途、创建和销毁时间。

  1. 线程私有区域 (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++ 编写的底层库)服务的。

  1. 线程共享区域 (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 程序在各种平台下对内存的访问都能达到一致的效果。

  1. JMM 的抽象架构
    JMM 定义了线程和主内存之间的抽象关系:

主内存 (Main Memory):

所有变量(共享变量)都存在这里。

对应物理硬件的 RAM(内存条)。

工作内存 (Working Memory):

每个线程私有。

保存了该线程使用到的变量的主内存副本。

对应物理硬件:寄存器 + L1/L2/L3 缓存。

交互规则:线程不能直接读写主内存,必须先在工作内存中操作,再同步回主内存。

  1. 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 定义的一系列天然的先行发生关系(如:启动线程的操作先于线程内的任何操作)。

第三部分:深度对比与关联

这是最容易混淆的部分,请仔细阅读。

  1. 两个"栈"的区别
    JVM 虚拟机栈:是数据结构。里面存的是栈帧,栈帧里有局部变量表。
    JMM 工作内存:是逻辑概念。它涵盖了 CPU 寄存器、L1/L2 缓存等。
    关联:通常情况下,JVM 栈中的局部变量(非共享)主要存在于 JMM 的工作内存(寄存器/缓存)中;而堆中的对象(共享)主要存在于主内存中,但也会被加载到工作内存(缓存)中。
  2. 两个"内存"的映射
    JMM 概念 对应的 JVM 区域 对应的物理硬件
    主内存 Java 堆 (Heap)、方法区 物理内存 (RAM)
    工作内存 虚拟机栈 (部分)、程序计数器 CPU 寄存器、L1/L2/L3 缓存
  3. 为什么需要两套模型?
    JVM 运行时数据区是为了管理内存的生命周期(分配、回收、结构化存储)。
    JMM 是为了解决并发编程的安全性(缓存一致性、指令重排序)。
    第四部分:总结与面试核心
    核心口诀
    JVM 内存分五块:堆、栈(Java栈/本地栈)、方法区、程序计数器。
    JMM 讲三点:原子性、可见性、有序性。
    堆是存对象的,栈是运行方法的。
    volatile 管可见和有序,synchronized 全都管。
    常见误区纠正
    误区:"栈是线程安全的,所以栈里的变量都在工作内存;堆是线程共享的,所以堆里的变量都在主内存。"
    纠正:不完全正确。JMM 中,堆中的对象副本也会进入工作内存(CPU 缓存)。如果 CPU 缓存不一致,堆里的对象也会出现线程安全问题。这就是为什么堆上的变量也需要 volatile 或锁来保证可见性。
相关推荐
这儿有个昵称2 小时前
互联网大厂Java面试场景:从Spring Boot到微服务架构
java·spring boot·消息队列·微服务架构·大厂面试·数据库优化
lsx2024062 小时前
Perl 错误处理
开发语言
甄心爱学习2 小时前
KMP算法(小白理解)
开发语言·python·算法
zephyr052 小时前
C++ STL unordered_set 与 unordered_map 完全指南
开发语言·数据结构·c++
填满你的记忆2 小时前
【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变
java·数据库·redis·分布式·缓存
lendsomething2 小时前
Spring 多数据源事务管理,JPA为例
java·数据库·spring·事务·jpa
Never_Satisfied2 小时前
在JavaScript / HTML中,HTML元素自定义属性使用指南
开发语言·javascript·html
Ulyanov2 小时前
大规模战场数据与推演:性能优化与多视图布局实战
开发语言·python·性能优化·tkinter·pyvista·gui开发
nsjqj2 小时前
JavaEE初阶:多线程初阶(2)
java·开发语言