抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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 # 初始堆大小(建议与-Xmx相同)
-Xmx2g # 最大堆大小

# 年轻代大小
-Xmn256m # 年轻代大小
-XX:NewRatio=2 # 老年代:年轻代 = 2:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1

# 晋升相关
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值
-XX:PretenureSizeThreshold=1000000 # 大对象直接进老年代

# TLAB
-XX:+UseTLAB # 开启TLAB(默认开启)
-XX:TLABSize=512k # TLAB大小

方法区 / 元空间(Metaspace)

存储类的元数据信息。

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 # 初始大小(触发GC的阈值)
-XX:MaxMetaspaceSize=512m # 最大大小(默认无限制)

# 压缩类空间(64位JVM,启用压缩指针时)
-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;

// 线程A执行
public void writer() {
value = 42; // 操作1
ready = true; // 操作2(volatile写)
}

// 线程B执行
public void reader() {
if (ready) { // 操作3(volatile读)
int result = value; // 操作4,一定能看到42
}
}
}
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 # Eden:Survivor比例

# 栈内存
-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
# 选择GC
-XX:+UseSerialGC # Serial
-XX:+UseParallelGC # Parallel(JDK 8默认)
-XX:+UseG1GC # G1(JDK 9+默认)
-XX:+UseZGC # ZGC
-XX:+UseShenandoahGC # Shenandoah

# G1参数
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:G1HeapRegionSize=4m # Region大小

# GC日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags

诊断参数

1
2
3
4
5
6
7
8
9
10
11
12
13
# OOM时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof

# OOM时执行脚本
-XX:OnOutOfMemoryError="kill -9 %p"

# 打印GC详情
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

# 打印类加载信息
-verbose:class

总结

区域 线程共享 内容 异常
程序计数器 私有 字节码行号
虚拟机栈 私有 栈帧(局部变量、操作数栈等) SOF/OOM
本地方法栈 私有 Native方法栈帧 SOF/OOM
共享 对象实例 OOM
方法区/元空间 共享 类信息、常量池等 OOM

JMM核心要点:

  • 可见性:一个线程的修改对其他线程可见
  • 有序性:禁止指令重排序
  • 原子性:操作不可被中断

保证线程安全的方式:

  • volatile:保证可见性和有序性
  • synchronized:保证原子性、可见性和有序性
  • final:保证不可变性
  • happens-before:定义操作间的可见性规则

理解JVM内存结构和Java内存模型是编写高性能、线程安全Java程序的基础。