【Java JVM】深入理解JVM内存结构与Java内存模型
JVM内存是Java程序运行的基础。本文将深入讲解JVM运行时数据区的结构,以及Java内存模型(JMM)的核心概念,帮助理解Java程序的内存管理机制。
概念区分
首先需要区分两个容易混淆的概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ┌──────────────────────────────────────────────────────────────────┐ │ JVM内存结构 vs Java内存模型 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ JVM内存结构(JVM Memory Structure) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ • 描述JVM运行时数据区的划分 │ │ │ │ • 包括堆、栈、方法区等 │ │ │ │ • 关注内存如何分配和组织 │ │ │ │ • 由JVM规范定义 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Java内存模型(Java Memory Model, JMM) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ • 描述多线程环境下的内存可见性 │ │ │ │ • 规定主内存与工作内存的交互 │ │ │ │ • 关注并发编程的内存语义 │ │ │ │ • 由Java语言规范定义 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
JVM运行时数据区
整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| ┌──────────────────────────────────────────────────────────────────┐ │ JVM运行时数据区 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 线程共享区域 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ 堆(Heap) │ │ │ │ │ │ ┌─────────────────────┐ ┌───────────────────────┐ │ │ │ │ │ │ │ 年轻代 │ │ 老年代 │ │ │ │ │ │ │ │ ┌────┬────┬────┐ │ │ │ │ │ │ │ │ │ │ │Eden│ S0 │ S1 │ │ │ Old Generation │ │ │ │ │ │ │ │ └────┴────┴────┘ │ │ │ │ │ │ │ │ │ └─────────────────────┘ └───────────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ 方法区 / 元空间(Metaspace) │ │ │ │ │ │ 类信息、常量、静态变量、JIT编译后的代码 │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 线程私有区域 │ │ │ │ │ │ │ │ Thread 1 Thread 2 Thread 3 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │程序计数器 │ │程序计数器 │ │程序计数器 │ │ │ │ │ ├──────────┤ ├──────────┤ ├──────────┤ │ │ │ │ │ 虚拟机栈 │ │ 虚拟机栈 │ │ 虚拟机栈 │ │ │ │ │ ├──────────┤ ├──────────┤ ├──────────┤ │ │ │ │ │本地方法栈 │ │本地方法栈 │ │本地方法栈 │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,记录当前线程执行的字节码行号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ┌──────────────────────────────────────────────────────────────────┐ │ 程序计数器 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 作用: │ │ • 字节码解释器通过PC决定下一条要执行的指令 │ │ • 线程切换后能恢复到正确的执行位置 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 执行过程示例 │ │ │ │ │ │ │ │ 字节码指令 PC值 │ │ │ │ ───────────────────────────────── │ │ │ │ 0: iconst_1 PC = 0 │ │ │ │ 1: istore_1 PC = 1 │ │ │ │ 2: iconst_2 PC = 2 │ │ │ │ 3: istore_2 PC = 3 │ │ │ │ 4: iload_1 PC = 4 │ │ │ │ 5: iload_2 PC = 5 │ │ │ │ 6: iadd PC = 6 │ │ │ │ 7: ireturn PC = 7 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 特点: │ │ • 线程私有,每个线程都有独立的PC │ │ • 执行Java方法时,PC记录字节码地址 │ │ • 执行Native方法时,PC值为空(Undefined) │ │ • 唯一不会发生OutOfMemoryError的区域 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
Java虚拟机栈(JVM Stack)
虚拟机栈描述Java方法执行的内存模型,每个方法调用对应一个栈帧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ┌──────────────────────────────────────────────────────────────────┐ │ 虚拟机栈 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 栈帧结构 │ │ │ │ │ │ │ │ 栈顶 ─▶ ┌─────────────────────────────────────────────┐ │ │ │ │ │ 当前栈帧 │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ 局部变量表 │ │ │ │ │ │ │ │ ┌─────┬─────┬─────┬─────┬─────┐ │ │ │ │ │ │ │ │ │this │ arg1│ arg2│ var1│ var2│ │ │ │ │ │ │ │ │ └─────┴─────┴─────┴─────┴─────┘ │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ 操作数栈 │ │ │ │ │ │ │ │ 用于计算过程中的临时数据存储 │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ 动态链接 │ │ │ │ │ │ │ │ 指向运行时常量池的方法引用 │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ 方法返回地址 │ │ │ │ │ │ │ │ 方法退出后返回的位置 │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ 调用者栈帧 │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ ... │ │ │ │ │ 栈底 ─▶ └─────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
局部变量表
存放编译期可知的各种数据类型和对象引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌──────────────────────────────────────────────────────────────────┐ │ 局部变量表 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 存储单位:变量槽(Slot),32位 │ │ │ │ 数据类型 占用Slot数 │ │ ───────────────────────────────────── │ │ boolean, byte, │ │ char, short, int, 1个Slot │ │ float, reference │ │ │ │ long, double 2个Slot(64位) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 示例:public int calc(int a, long b, Object c) │ │ │ │ │ │ │ │ Slot 内容 │ │ │ │ ────────────────────────────────── │ │ │ │ 0 this(实例方法隐含参数) │ │ │ │ 1 int a │ │ │ │ 2-3 long b(占2个slot) │ │ │ │ 4 Object c │ │ │ │ 5+ 局部变量... │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Slot复用: │ │ • 局部变量超出作用域后,其Slot可被其他变量复用 │ │ • 可能影响GC(Slot未被覆盖时仍持有引用) │ │ │ └──────────────────────────────────────────────────────────────────┘
|
操作数栈
用于方法执行过程中的计算。
1 2 3 4
| int a = 1; int b = 2; int c = a + b;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ┌──────────────────────────────────────────────────────────────────┐ │ 操作数栈执行过程 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 指令 操作数栈(栈顶在右) 局部变量表 │ │ ─────────────────────────────────────────────────────────────── │ │ │ │ iconst_1 [1] [_, _, _, _] │ │ istore_1 [] [_, 1, _, _] // a=1 │ │ iconst_2 [2] [_, 1, _, _] │ │ istore_2 [] [_, 1, 2, _] // b=2 │ │ iload_1 [1] [_, 1, 2, _] // 加载a │ │ iload_2 [1, 2] [_, 1, 2, _] // 加载b │ │ iadd [3] [_, 1, 2, _] // a+b │ │ istore_3 [] [_, 1, 2, 3] // c=3 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
栈异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌──────────────────────────────────────────────────────────────────┐ │ 栈异常 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ StackOverflowError │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ • 线程请求的栈深度超过虚拟机允许的最大深度 │ │ │ │ • 常见原因:递归调用没有正确退出条件 │ │ │ │ │ │ │ │ public void recursive() { │ │ │ │ recursive(); // 无限递归导致StackOverflowError │ │ │ │ } │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ OutOfMemoryError │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ • 栈可以动态扩展时,无法申请到足够内存 │ │ │ │ • 创建大量线程时可能发生(每个线程需要独立的栈空间) │ │ │ │ │ │ │ │ // 创建大量线程导致OOM │ │ │ │ while (true) { │ │ │ │ new Thread(() -> { │ │ │ │ try { Thread.sleep(Long.MAX_VALUE); } │ │ │ │ catch (Exception e) {} │ │ │ │ }).start(); │ │ │ │ } │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 相关参数:-Xss256k // 设置每个线程的栈大小 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
本地方法栈(Native Method Stack)
为Native方法服务,与虚拟机栈类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ┌──────────────────────────────────────────────────────────────────┐ │ 本地方法栈 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Java方法 Native方法 │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ 虚拟机栈 本地方法栈 │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ 栈帧 │ │ 栈帧 │ │ │ │ │ │ (Java) │ │(Native) │ │ │ │ │ └─────────┘ └─────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 特点: │ │ • 线程私有 │ │ • HotSpot VM将虚拟机栈和本地方法栈合二为一 │ │ • 也会抛出StackOverflowError和OutOfMemoryError │ │ │ │ Native方法示例: │ │ • System.currentTimeMillis() │ │ • Object.hashCode() │ │ • Thread.start0() │ │ │ └──────────────────────────────────────────────────────────────────┘
|
堆(Heap)
堆是JVM管理的最大内存区域,存放对象实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| ┌──────────────────────────────────────────────────────────────────┐ │ 堆内存 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 堆内存布局 │ │ │ │ │ │ │ │ ┌─────────────────────────────┐ ┌──────────────────────┐ │ │ │ │ │ 年轻代(Young) │ │ 老年代(Old) │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ │ │ │ │ Eden │ │ │ │ │ │ │ │ │ │ (新对象分配区) │ │ │ (长期存活对象) │ │ │ │ │ │ │ 80% │ │ │ │ │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │ │ │ │Survivor0│ │Survivor1│ │ │ │ │ │ │ │ │ │ (From) │ │ (To) │ │ │ │ │ │ │ │ │ │ 10% │ │ 10% │ │ │ │ │ │ │ │ │ └─────────┘ └─────────┘ │ │ │ │ │ │ │ └─────────────────────────────┘ └──────────────────────┘ │ │ │ │ 默认 1/3 默认 2/3 │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
对象分配流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ┌──────────────────────────────────────────────────────────────────┐ │ 对象分配流程 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ new Object() │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ 是否开启TLAB? │ │ │ └────────┬────────┘ │ │ 是 │ │ 否 │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │TLAB分配 │ │Eden区分配 │ │ │ │(线程本地缓冲)│ │(CAS竞争) │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ └───────┬────────┘ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ 分配成功? │ │ │ └────────┬────────┘ │ │ 是 │ │ 否 │ │ ▼ ▼ │ │ ┌─────┐ ┌─────────────┐ │ │ │完成 │ │ Minor GC │ │ │ └─────┘ └──────┬──────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ 还是分配失败? │ │ │ └────────┬────────┘ │ │ 是 │ │ 否 │ │ ▼ ▼ │ │ ┌──────────┐ ┌─────┐ │ │ │老年代分配│ │完成 │ │ │ └──────────┘ └─────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
TLAB(Thread Local Allocation Buffer)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ┌──────────────────────────────────────────────────────────────────┐ │ TLAB │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 问题:多线程同时在Eden区分配对象需要同步,影响效率 │ │ │ │ 解决:TLAB - 为每个线程在Eden区预留一块私有缓冲区 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Eden区 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │ │ │ │Thread 1 │ │Thread 2 │ │Thread 3 │ │ 公共区域 │ │ │ │ │ │ TLAB │ │ TLAB │ │ TLAB │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 特点: │ │ • 线程在自己的TLAB分配无需同步 │ │ • TLAB用完后再申请新的TLAB │ │ • TLAB通常很小(Eden的1%以内) │ │ • 默认开启:-XX:+UseTLAB │ │ │ └──────────────────────────────────────────────────────────────────┘
|
对象晋升
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ┌──────────────────────────────────────────────────────────────────┐ │ 对象晋升到老年代 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 晋升条件: │ │ │ │ 1. 年龄达到阈值 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 对象每经历一次Minor GC且存活,年龄+1 │ │ │ │ 年龄达到MaxTenuringThreshold(默认15)时晋升 │ │ │ │ 参数:-XX:MaxTenuringThreshold=15 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ 2. 动态年龄判断 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Survivor区中相同年龄对象大小总和 > Survivor空间的50% │ │ │ │ 则该年龄及以上的对象直接晋升 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ 3. 大对象直接进入老年代 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 对象大小超过PretenureSizeThreshold │ │ │ │ 参数:-XX:PretenureSizeThreshold=1000000 (约1MB) │ │ │ │ 避免在Eden和Survivor之间大量复制 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ 4. Survivor空间不足 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Minor GC后存活对象无法放入Survivor │ │ │ │ 通过分配担保进入老年代 │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
堆内存参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| -Xms512m -Xmx2g
-Xmn256m -XX:NewRatio=2 -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=1000000
-XX:+UseTLAB -XX:TLABSize=512k
|
存储类的元数据信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| ┌──────────────────────────────────────────────────────────────────┐ │ 方法区的演变 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ JDK 7及之前:永久代(PermGen) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 堆内存 │ │ │ │ ┌──────────────────┐ ┌────────────────────────────────┐│ │ │ │ │ 年轻代 + 老年代 │ │ 永久代(PermGen) ││ │ │ │ │ │ │ • 类信息 ││ │ │ │ │ │ │ • 常量池 ││ │ │ │ │ │ │ • 静态变量 ││ │ │ │ │ │ │ • 方法数据 ││ │ │ │ └──────────────────┘ └────────────────────────────────┘│ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 问题: │ │ • 永久代大小固定,容易OOM │ │ • 字符串常量池在永久代,不易回收 │ │ • GC效率低 │ │ │ │ │ │ JDK 8及之后:元空间(Metaspace) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 堆内存 本地内存 │ │ │ │ ┌──────────────────┐ ┌────────────────────┐ │ │ │ │ │ 年轻代 + 老年代 │ │ 元空间 │ │ │ │ │ │ │ │ (Metaspace) │ │ │ │ │ │ 字符串常量池 │ │ • 类信息 │ │ │ │ │ │ 静态变量 │ │ • 方法数据 │ │ │ │ │ │ │ │ • 常量池引用 │ │ │ │ │ └──────────────────┘ └────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 优点: │ │ • 使用本地内存,不受堆大小限制 │ │ • 可以动态扩展 │ │ • 字符串常量池移到堆中,便于GC │ │ │ └──────────────────────────────────────────────────────────────────┘
|
方法区存储内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ┌──────────────────────────────────────────────────────────────────┐ │ 方法区存储内容 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 1. 类信息(Klass) │ │ • 类的全限定名 │ │ • 父类的全限定名 │ │ • 实现的接口列表 │ │ • 字段信息(名称、类型、修饰符) │ │ • 方法信息(名称、返回类型、参数、修饰符) │ │ • 访问修饰符 │ │ │ │ 2. 运行时常量池 │ │ • 编译期生成的字面量 │ │ • 符号引用(运行时解析为直接引用) │ │ │ │ 3. JIT编译后的代码 │ │ • 热点代码的本地机器码 │ │ │ │ 4. 静态变量(JDK 7+移到堆中) │ │ │ └──────────────────────────────────────────────────────────────────┘
|
元空间参数
1 2 3 4 5 6
| -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:CompressedClassSpaceSize=256m
|
直接内存(Direct Memory)
直接内存不属于JVM运行时数据区,但也被频繁使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ┌──────────────────────────────────────────────────────────────────┐ │ 直接内存 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 使用场景:NIO的DirectByteBuffer │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 传统IO vs NIO │ │ │ │ │ │ │ │ 传统IO: │ │ │ │ 磁盘 ──▶ 内核缓冲区 ──▶ JVM堆 ──▶ 应用程序 │ │ │ │ (复制1) (复制2) │ │ │ │ │ │ │ │ NIO(直接内存): │ │ │ │ 磁盘 ──▶ 直接内存 ──▶ 应用程序 │ │ │ │ (零拷贝) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 代码示例: │ │ ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); │ │ │ │ 优点: │ │ • 减少数据拷贝次数 │ │ • 适合大文件或频繁IO操作 │ │ │ │ 缺点: │ │ • 分配和回收开销大 │ │ • 不受JVM GC直接管理 │ │ │ │ 参数:-XX:MaxDirectMemorySize=256m │ │ │ └──────────────────────────────────────────────────────────────────┘
|
对象的内存布局
对象在内存中的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| ┌──────────────────────────────────────────────────────────────────┐ │ 对象内存布局 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 普通对象 │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ 对象头(Header) │ │ │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Mark Word (8字节) │ │ │ │ │ │ │ │ 哈希码、GC年龄、锁状态、线程ID等 │ │ │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 类型指针 (4/8字节) │ │ │ │ │ │ │ │ 指向类元数据,确定对象属于哪个类 │ │ │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ 实例数据(Instance Data) │ │ │ │ │ │ 对象真正存储的有效信息,各字段内容 │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ 对齐填充(Padding) │ │ │ │ │ │ 保证对象大小是8字节的整数倍 │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 数组对象 │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ Mark Word + 类型指针 + 数组长度(4字节) + 数组数据 │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
Mark Word详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ┌──────────────────────────────────────────────────────────────────┐ │ Mark Word结构(64位JVM) │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 锁状态 Mark Word内容(64位) │ │ │ │─────────────────────────────────────────────────────────── │ │ │ │ │ │ │ │ 无锁 │ unused:25│hashcode:31│unused:1│age:4│0│01│ │ │ │ │ │ │ │ 偏向锁 │ threadId:54 │ epoch:2 │unused:1│age:4│1│01│ │ │ │ │ │ │ │ 轻量级锁 │ 指向栈中锁记录的指针 │ 00 │ │ │ │ │ │ │ │ 重量级锁 │ 指向Monitor的指针 │ 10 │ │ │ │ │ │ │ │ GC标记 │ │ 11 │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ 字段说明: │ │ • hashcode: 对象的hashCode(调用后才有) │ │ • age: GC年龄(最大15) │ │ • threadId: 偏向的线程ID │ │ • epoch: 偏向时间戳 │ │ • 最后2位: 锁标志位 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
压缩指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ┌──────────────────────────────────────────────────────────────────┐ │ 压缩指针 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 64位JVM默认开启压缩指针(堆<32GB时) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 未压缩 vs 压缩 │ │ │ │ │ │ │ │ 未压缩(-XX:-UseCompressedOops): │ │ │ │ • 对象引用:8字节 │ │ │ │ • 类型指针:8字节 │ │ │ │ │ │ │ │ 压缩(-XX:+UseCompressedOops): │ │ │ │ • 对象引用:4字节 │ │ │ │ • 类型指针:4字节 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 原理: │ │ • 对象按8字节对齐,低3位始终为0 │ │ • 存储时右移3位,使用时左移3位恢复 │ │ • 32位可寻址 2^32 × 8 = 32GB │ │ │ │ 示例(Object对象大小): │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 压缩指针开启:Mark Word(8) + 类型指针(4) + 填充(4) = 16│ │ │ │ 压缩指针关闭:Mark Word(8) + 类型指针(8) = 16 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
Java内存模型(JMM)
JMM概述
Java内存模型定义了多线程环境下共享变量的访问规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌──────────────────────────────────────────────────────────────────┐ │ Java内存模型 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Thread 1 Thread 2 Thread 3 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ 工作内存 │ │ 工作内存 │ │ 工作内存 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ x的副本 │ │ x的副本 │ │ x的副本 │ │ │ │ │ │ y的副本 │ │ y的副本 │ │ y的副本 │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ │ read/write │ │ │ │ │ │ │ load/store │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ 主内存 │ │ │ │ │ │ │ │ │ │ │ │ 共享变量 x 共享变量 y ... │ │ │ │ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ 注意:JMM是抽象概念,不等同于JVM内存结构 │ │ • 主内存 ≈ 堆内存中的共享变量 │ │ • 工作内存 ≈ 线程栈中的变量副本(寄存器、缓存等) │ │ │ └──────────────────────────────────────────────────────────────────┘
|
内存交互操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ┌──────────────────────────────────────────────────────────────────┐ │ JMM内存交互操作 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ JMM定义了8种原子操作: │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 主内存 工作内存 │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ │ │ │ ────read────▶ │ │ │ │ │ │ │ 变量x │ ◀───write─── │ x副本 │ │ │ │ │ │ │ │ │ │ │ │ │ │ lock │ load ───▶ │ use ───▶ 线程 │ │ │ │ │ unlock │ ◀─── store │ ◀─── assign │ │ │ │ │ │ │ │ │ │ │ │ └──────────┘ └──────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 操作说明: │ │ ─────────────────────────────────────────────────────────────── │ │ lock 把主内存变量标识为线程独占 │ │ unlock 释放主内存变量的独占状态 │ │ read 从主内存读取变量值到工作内存 │ │ load 把read的值放入工作内存的变量副本 │ │ use 把工作内存变量值传给执行引擎 │ │ assign 把执行引擎的值赋给工作内存变量 │ │ store 把工作内存变量值传送到主内存 │ │ write 把store的值写入主内存变量 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
可见性问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| ┌──────────────────────────────────────────────────────────────────┐ │ 可见性问题 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 问题示例: │ │ │ │ public class VisibilityDemo { │ │ private boolean flag = true; │ │ │ │ public void stop() { │ │ flag = false; // 线程B修改 │ │ } │ │ │ │ public void run() { │ │ while (flag) { // 线程A可能永远看不到修改 │ │ // do something │ │ } │ │ } │ │ } │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 执行过程 │ │ │ │ │ │ │ │ 线程A 线程B │ │ │ │ ┌────────────┐ ┌────────────┐ │ │ │ │ │flag=true │ │flag=true │ │ │ │ │ │(工作内存) │ │(工作内存) │ │ │ │ │ └────────────┘ └────────────┘ │ │ │ │ ▲ │ │ │ │ │ │ 一直读本地副本 │ flag=false │ │ │ │ │ 看不到修改! ▼ │ │ │ │ ┌────────────────────┐ │ │ │ │ │ 主内存 │ │ │ │ │ │ flag=false │ │ │ │ │ └────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
volatile关键字
volatile保证可见性和有序性,但不保证原子性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ┌──────────────────────────────────────────────────────────────────┐ │ volatile │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 1. 可见性保证 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ private volatile boolean flag = true; │ │ │ │ │ │ │ │ • 写volatile变量时,强制刷新到主内存 │ │ │ │ • 读volatile变量时,强制从主内存读取 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 2. 有序性保证(禁止指令重排) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ // 双重检查锁定的正确写法 │ │ │ │ private volatile static Singleton instance; │ │ │ │ │ │ │ │ public static Singleton getInstance() { │ │ │ │ if (instance == null) { │ │ │ │ synchronized (Singleton.class) { │ │ │ │ if (instance == null) { │ │ │ │ instance = new Singleton(); │ │ │ │ // 没有volatile可能发生重排序 │ │ │ │ // 1.分配内存 2.初始化 3.赋值引用 │ │ │ │ // 可能重排为 1→3→2,导致返回未初始化对象│ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ return instance; │ │ │ │ } │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 3. 不保证原子性 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ private volatile int count = 0; │ │ │ │ │ │ │ │ count++; // 不是原子操作! │ │ │ │ // 实际是:读取count → 加1 → 写回count │ │ │ │ // 多线程下仍然会出问题 │ │ │ │ │ │ │ │ 解决:使用AtomicInteger或synchronized │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
volatile的内存屏障
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ┌──────────────────────────────────────────────────────────────────┐ │ volatile内存屏障 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 内存屏障类型: │ │ ─────────────────────────────────────────────────────────────── │ │ LoadLoad 确保Load1数据装载先于Load2及后续装载 │ │ StoreStore 确保Store1数据刷新先于Store2及后续存储 │ │ LoadStore 确保Load1数据装载先于Store2及后续存储 │ │ StoreLoad 确保Store1数据刷新先于Load2及后续装载(最强屏障) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ volatile写操作 │ │ │ │ │ │ │ │ 普通读/写操作 │ │ │ │ ──────────────── │ │ │ │ StoreStore屏障 // 禁止上面普通写与volatile写重排 │ │ │ │ ──────────────── │ │ │ │ volatile写 │ │ │ │ ──────────────── │ │ │ │ StoreLoad屏障 // 禁止volatile写与下面读/写重排 │ │ │ │ ──────────────── │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ volatile读操作 │ │ │ │ │ │ │ │ volatile读 │ │ │ │ ──────────────── │ │ │ │ LoadLoad屏障 // 禁止volatile读与下面普通读重排 │ │ │ │ ──────────────── │ │ │ │ LoadStore屏障 // 禁止volatile读与下面普通写重排 │ │ │ │ ──────────────── │ │ │ │ 普通读/写操作 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
happens-before原则
happens-before定义了操作之间的可见性关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ┌──────────────────────────────────────────────────────────────────┐ │ happens-before原则 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 如果操作A happens-before 操作B,那么: │ │ A的结果对B可见,且A的执行顺序在B之前 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 8条规则 │ │ │ │ │ │ │ │ 1. 程序顺序规则 │ │ │ │ 同一线程中,前面的操作 happens-before 后面的操作 │ │ │ │ │ │ │ │ 2. 监视器锁规则 │ │ │ │ unlock操作 happens-before 后续的lock操作 │ │ │ │ │ │ │ │ 3. volatile变量规则 │ │ │ │ volatile写 happens-before 后续的volatile读 │ │ │ │ │ │ │ │ 4. 线程启动规则 │ │ │ │ Thread.start() happens-before 线程中的所有操作 │ │ │ │ │ │ │ │ 5. 线程终止规则 │ │ │ │ 线程中所有操作 happens-before Thread.join()返回 │ │ │ │ │ │ │ │ 6. 线程中断规则 │ │ │ │ interrupt()调用 happens-before 被中断线程检测到中断 │ │ │ │ │ │ │ │ 7. 对象终结规则 │ │ │ │ 构造函数结束 happens-before finalize()开始 │ │ │ │ │ │ │ │ 8. 传递性规则 │ │ │ │ A happens-before B,B happens-before C │ │ │ │ 则 A happens-before C │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘
|
happens-before示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class HappensBeforeDemo { private int value = 0; private volatile boolean ready = false;
public void writer() { value = 42; ready = true; }
public void reader() { if (ready) { int result = value; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ┌──────────────────────────────────────────────────────────────────┐ │ happens-before推导 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 线程A 线程B │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 操作1: value=42 │ │ 操作3: if(ready) │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ │ │ 操作2: ready=true│───volatile───▶│ 操作4: value │ │ │ └─────────────────┘ 规则 └─────────────────┘ │ │ │ │ 推导过程: │ │ 1. 操作1 happens-before 操作2(程序顺序规则) │ │ 2. 操作2 happens-before 操作3(volatile规则) │ │ 3. 操作3 happens-before 操作4(程序顺序规则) │ │ 4. 由传递性:操作1 happens-before 操作4 │ │ │ │ 结论:线程B在操作4一定能看到线程A在操作1写入的value=42 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
常见内存问题与排查
内存溢出类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| ┌──────────────────────────────────────────────────────────────────┐ │ 内存溢出类型 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 1. 堆内存溢出 │ │ java.lang.OutOfMemoryError: Java heap space │ │ 原因:对象太多或太大,堆内存不足 │ │ 排查:增加堆内存、分析堆转储文件 │ │ │ │ 2. 元空间溢出 │ │ java.lang.OutOfMemoryError: Metaspace │ │ 原因:加载的类太多(如动态代理、热部署) │ │ 排查:增加元空间大小、检查类加载器泄漏 │ │ │ │ 3. 栈溢出 │ │ java.lang.StackOverflowError │ │ 原因:递归过深、方法调用链太长 │ │ 排查:检查递归逻辑、增加栈大小 │ │ │ │ 4. 直接内存溢出 │ │ java.lang.OutOfMemoryError: Direct buffer memory │ │ 原因:NIO DirectByteBuffer使用过多 │ │ 排查:增加直接内存限制、检查Buffer是否正确释放 │ │ │ │ 5. GC开销超限 │ │ java.lang.OutOfMemoryError: GC overhead limit exceeded │ │ 原因:GC时间占比过高(>98%),但回收效果差(<2%) │ │ 排查:分析内存泄漏、优化对象创建 │ │ │ │ 6. 无法创建线程 │ │ java.lang.OutOfMemoryError: unable to create native thread │ │ 原因:线程数超限或内存不足以创建新线程 │ │ 排查:减少线程数、减小栈大小、增加系统内存 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
内存泄漏常见场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ┌──────────────────────────────────────────────────────────────────┐ │ 内存泄漏常见场景 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 1. 静态集合类持有对象引用 │ │ static List<Object> list = new ArrayList<>(); │ │ list.add(obj); // 对象永远不会被回收 │ │ │ │ 2. 未关闭的资源 │ │ Connection conn = getConnection(); │ │ // 忘记conn.close() │ │ │ │ 3. ThreadLocal未清理 │ │ threadLocal.set(obj); │ │ // 忘记threadLocal.remove() │ │ │ │ 4. 监听器未移除 │ │ button.addActionListener(listener); │ │ // 忘记removeActionListener │ │ │ │ 5. 内部类持有外部类引用 │ │ public class Outer { │ │ private byte[] data = new byte[1024*1024]; │ │ class Inner { // 隐式持有Outer.this │ │ } │ │ } │ │ │ │ 6. 缓存未设置上限或过期策略 │ │ Map<String, Object> cache = new HashMap<>(); │ │ // 缓存无限增长 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
排查工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ┌──────────────────────────────────────────────────────────────────┐ │ 内存排查工具 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 命令行工具: │ │ ─────────────────────────────────────────────────────────────── │ │ jps 查看Java进程 │ │ jstat 监控GC统计信息 │ │ jstat -gc <pid> 1000 10 │ │ jmap 生成堆转储文件 │ │ jmap -dump:format=b,file=heap.hprof <pid> │ │ jstack 查看线程堆栈 │ │ jstack <pid> > thread.txt │ │ jinfo 查看JVM参数 │ │ jinfo -flags <pid> │ │ │ │ 图形化工具: │ │ ─────────────────────────────────────────────────────────────── │ │ VisualVM 综合监控工具 │ │ JConsole JMX监控 │ │ MAT 内存分析(Eclipse Memory Analyzer) │ │ JProfiler 商业性能分析工具 │ │ Arthas 阿里开源诊断工具 │ │ │ │ 常用命令示例: │ │ ─────────────────────────────────────────────────────────────── │ │ # 查看GC情况 │ │ jstat -gcutil <pid> 1000 │ │ │ │ # 生成堆转储(OOM时自动生成) │ │ -XX:+HeapDumpOnOutOfMemoryError │ │ -XX:HeapDumpPath=/path/to/dump │ │ │ │ # 查看对象统计 │ │ jmap -histo <pid> | head -20 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
JVM参数汇总
内存相关参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| -Xms4g -Xmx4g -Xmn1g -XX:NewRatio=2 -XX:SurvivorRatio=8
-Xss256k
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=256m
-XX:+UseCompressedOops -XX:+UseCompressedClassPointers
|
GC相关参数
1 2 3 4 5 6 7 8 9 10 11 12 13
| -XX:+UseSerialGC -XX:+UseParallelGC -XX:+UseG1GC -XX:+UseZGC -XX:+UseShenandoahGC
-XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m
-Xlog:gc*:file=gc.log:time,uptime,level,tags
|
诊断参数
1 2 3 4 5 6 7 8 9 10 11 12 13
| -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
-XX:OnOutOfMemoryError="kill -9 %p"
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
-verbose:class
|
总结
| 区域 |
线程共享 |
内容 |
异常 |
| 程序计数器 |
私有 |
字节码行号 |
无 |
| 虚拟机栈 |
私有 |
栈帧(局部变量、操作数栈等) |
SOF/OOM |
| 本地方法栈 |
私有 |
Native方法栈帧 |
SOF/OOM |
| 堆 |
共享 |
对象实例 |
OOM |
| 方法区/元空间 |
共享 |
类信息、常量池等 |
OOM |
JMM核心要点:
- 可见性:一个线程的修改对其他线程可见
- 有序性:禁止指令重排序
- 原子性:操作不可被中断
保证线程安全的方式:
- volatile:保证可见性和有序性
- synchronized:保证原子性、可见性和有序性
- final:保证不可变性
- happens-before:定义操作间的可见性规则
理解JVM内存结构和Java内存模型是编写高性能、线程安全Java程序的基础。