Skip to content

JVM 内存模型

约 1610 字大约 5 分钟

JVM字节美团

2025-04-17

⭐ 题目日期:

美团 - 2025/4/12,字节 - 2024/12/10

📝 题解:


JVM 内存模型详解

JVM 内存模型(Java Virtual Machine Memory Model)通常指两个层面:

  1. JVM 运行时数据区域:描述 JVM 在运行时的内存划分。
  2. Java 内存模型(JMM, Java Memory Model):定义多线程环境下共享变量的访问规则(可见性、有序性、原子性)。

以下从这两个层面详细解析,符合面试要求的深度和结构:


一、JVM 运行时数据区域

JVM 运行时数据区域分为 线程共享区线程私有区,结构如下:

区域作用线程共享性异常
程序计数器记录当前线程执行的位置(字节码行号指示器),分支、循环、跳转依赖此区域。线程私有
虚拟机栈(Java栈)存储方法调用的栈帧(局部变量表、操作数栈、动态链接、方法出口等)。线程私有StackOverflowError(栈溢出)
OutOfMemoryError(无法扩展栈)
本地方法栈为 Native 方法(如C/C++实现的方法)服务,与虚拟机栈类似。线程私有同上
堆(Heap)存放对象实例和数组,是垃圾回收的主要区域。线程共享OutOfMemoryError(堆内存耗尽)
方法区存储类元数据(Class结构)、常量、静态变量、即时编译器编译后的代码等。线程共享OutOfMemoryError(元空间/永久代内存不足)

核心细节

  1. 堆(Heap)
    分代设计:分为 年轻代(Young Generation)老年代(Old Generation)
    年轻代:新对象在此分配,分为 Eden区 和两个 Survivor区(S0/S1)
    老年代:长期存活的对象(经过多次Minor GC未被回收)晋升至此。
    参数控制
    -Xms:初始堆大小(默认物理内存1/64)。
    -Xmx:最大堆大小(默认物理内存1/4)。
    -Xmn:年轻代大小(建议为堆的1/3~1/4)。

  2. 方法区
    实现变化
    JDK8 之前:通过永久代(PermGen)实现,容易触发 OutOfMemoryError: PermGen space
    JDK8+:改为元空间(Metaspace),使用本地内存(Native Memory),默认无上限,通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 限制。
    运行时常量池:原属于方法区的一部分,存放字面量和符号引用。

  3. 虚拟机栈
    栈帧结构:每个方法调用对应一个栈帧。
    局部变量表:存放方法参数和局部变量(基本类型和对象引用)。
    操作数栈:执行字节码指令的工作区(如算术运算、方法调用)。
    参数控制-Xss 设置栈大小(默认1MB,Linux-x64)。

  4. 直接内存(Direct Memory)
    • 通过 NIOByteBuffer.allocateDirect() 分配,属于堆外内存,不受JVM堆限制。
    异常OutOfMemoryError(通过 -XX:MaxDirectMemorySize 限制)。


二、Java 内存模型(JMM)

JMM 是《Java语言规范》的一部分,定义了多线程访问共享变量时的行为规则,核心目标是解决并发编程中的可见性、有序性和原子性问题

1. 主内存与工作内存

主内存(Main Memory):所有共享变量存储的位置。
工作内存(Working Memory):每个线程私有的内存区域,保存该线程使用的主内存副本。
交互规则:线程通过以下操作与主内存交互:
read:从主内存读取变量到工作内存。
load:将读取的值放入工作内存的变量副本。
use:将工作内存中的变量传递给执行引擎(如CPU)。
assign:将执行引擎计算后的值赋给工作内存中的变量。
store:将工作内存中的变量传回主内存。
write:将传回的值写入主内存中的变量。

2. 内存屏障与指令重排序

指令重排序:编译器或处理器优化可能导致代码执行顺序改变。
内存屏障(Memory Barrier):禁止特定类型的重排序,确保内存可见性。
LoadLoad:确保屏障前的读操作先于屏障后的读操作。
StoreStore:确保屏障前的写操作先于屏障后的写操作。
LoadStore:确保屏障前的读操作先于屏障后的写操作。
StoreLoad:确保屏障前的写操作对后续所有操作可见(开销最大)。

3. Happens-Before 原则

定义操作之间的偏序关系,若操作A happens-before 操作B,则A的结果对B可见。
规则示例
程序顺序规则:同一线程内的操作按代码顺序执行。
锁规则:解锁操作 happens-before 后续加锁操作。
volatile规则:volatile变量的写操作 happens-before 后续读操作。
线程启动规则Thread.start() happens-before 新线程的所有操作。
传递性规则:若A happens-before B,且B happens-before C,则A happens-before C。

4. volatile 关键字

可见性:volatile变量的修改立即对其他线程可见。
禁止指令重排序:通过插入内存屏障实现。
不保证原子性:如volatile int i=0; i++仍需同步(i++是读-改-写复合操作)。

5. final 关键字的内存语义

• 初始化完成的final字段对其他线程可见(无需同步)。
• 防止对象引用逃逸(如构造函数中未正确发布对象)。


三、常见问题与调优

1. 内存溢出(OOM)场景

堆溢出:对象过多或内存泄漏(如静态集合持有对象)。
元空间溢出:加载过多类(如动态生成类未卸载)。
栈溢出:无限递归或栈帧过大(如局部变量过多)。

2. 调优策略

堆内存:根据应用对象生命周期调整 -Xms-Xmx,避免频繁Full GC。
线程栈:减少 -Xss 值以支持更多线程(需平衡栈深度需求)。
元空间:限制 -XX:MaxMetaspaceSize 防止本地内存耗尽。
垃圾收集器:根据吞吐量或延迟需求选择(如G1、ZGC)。

3. 工具监控

命令行工具
jstat:监控堆和GC状态(如 jstat -gcutil <pid>)。
jmap:生成堆转储文件(jmap -dump:format=b,file=heap.bin <pid>)。
图形工具
VisualVM:实时监控内存、线程和CPU。
MAT(Memory Analyzer):分析堆转储,定位内存泄漏。


四、总结

运行时数据区域:关注堆、栈、方法区的划分及异常场景。
JMM:理解多线程下内存可见性、有序性的保障机制(如happens-before、volatile)。
调优核心:平衡内存分配与垃圾回收效率,结合工具定位瓶颈。