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 或锁来保证可见性。
相关推荐
侠客行031710 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪10 小时前
深入浅出LangChain4J
java·langchain·llm
灰子学技术12 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
老毛肚12 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎13 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰13 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码13 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚13 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂13 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas13613 小时前
41-parse的实现原理&有限状态机
开发语言·前端·javascript