一、JVM的概念
JVM(Java Virtual Machine,Java虚拟机) 是运行Java字节码(.class
文件)的抽象计算机,是Java跨平台("一次编写,到处运行")的核心。它屏蔽了底层操作系统和硬件的差异,通过以下机制实现:
1. 核心功能
-
字节码执行:将Java源码编译成的
.class
文件解释为机器码(或JIT编译为本地代码)。 -
内存管理:自动分配/回收内存(垃圾回收/GC),避免手动管理内存泄漏。
-
安全沙箱:通过字节码验证器、类加载器等机制防止恶意代码破坏系统。
2. 关键组成
组件 | 作用 |
---|---|
类加载器 | 动态加载类文件(如AppClassLoader 、ExtClassLoader )。 |
运行时数据区 | 划分内存区域: |
执行引擎 | 解释字节码或JIT编译为本地机器码(HotSpot JVM的C1/C2编译器)。 |
垃圾回收器 | 自动回收无用对象(如G1、ZGC、Shenandoah等算法)。 |
3. 跨平台原理
-
编译阶段:Java源码(
.java
)→ 平台无关的字节码(.class
)。 -
运行阶段:不同平台的JVM(如Windows/Linux的HotSpot)将字节码转换为对应机器码。
4. 常见实现
-
HotSpot(Oracle/OpenJDK默认):支持JIT和自适应优化。
-
OpenJ9(IBM):低内存占用,适合云原生。
-
GraalVM:多语言支持(Java、Python、JS),原生镜像(AOT编译)。
5. 调优场景示例
-
内存溢出:通过
-Xmx
调整堆大小,用jmap
分析堆转储。 -
GC优化:选择低延迟收集器(如ZGC)或吞吐量优先(Parallel GC)。
总而言之JVM是Java的"操作系统",通过字节码+虚拟化技术实现跨平台,同时提供自动内存管理和安全隔离。
二、JVM体系结构
注意:PC寄存器(程序计数器)内存非常小,接近可以忽略不计
三、类加载器以及双亲委派机制
java早期称为C++--:在C++的机制上去除繁琐的东西,如指针、内存管理等
3.1、类加载器
类加载器(Class Loader) 是 JVM 的一个子系统,负责 在运行时动态加载 .class
文件(字节码)到内存中,并为其在方法区中创建对应的 java.lang.Class
对象。它是 Java 动态性(如运行时加载、热部署)和安全沙箱机制的核心。
1. 核心职责
-
加载:读取字节码(从文件、网络、数据库等)。
-
验证:确保字节码符合 JVM 规范(防止恶意代码)。
-
初始化:为静态变量赋初值,执行静态代码块。
2. 双亲委派模型(Parent Delegation Model)
JVM 采用双亲委派机制避免重复加载和核心类被篡改:
Bootstrap Class Loader(启动类加载器)
↑
Platform Class Loader(平台类加载器,Java 9+)
↑
Application Class Loader(应用程序类加载器)
↑
Custom Class Loader(用户自定义类加载器)
工作流程
-
子类先委托父类加载:当一个类需要被加载时,子加载器会先请求父加载器尝试加载。
-
父类无法加载时才自己加载:如果父加载器找不到类(如非核心类),子加载器才会自己加载。
3. 三种默认类加载器(应用加载器、扩展加载器、根加载器)
类加载器 | 加载范围 | 实现语言 |
---|---|---|
Bootstrap | %JAVA_HOME%/lib/rt.jar (核心类库,如 java.lang.* ) | C++(Java无法查询到C或C++,所以通过getClassLoader()方法时,获取结果为null) |
Platform | %JAVA_HOME%/lib/ext/*.jar (扩展库) | Java |
Application | 用户 CLASSPATH 下的类(项目代码、第三方库) | Java |
4. 自定义类加载器
-
场景:实现 热部署(Tomcat 的
WebAppClassLoader
)、加密类文件加载、从数据库/网络加载类。 -
关键方法
public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = loadFromCustomSource(name); // 从自定义来源读取字节码 return defineClass(name, bytes, 0, bytes.length); } }
5. 破坏双亲委派模型(百度的,我也不太懂)
-
SPI(Service Provider Interface)机制(如 JDBC):
-
核心类(如
java.sql.Driver
)由 Bootstrap 加载,但实现类(如com.mysql.Driver
)由 AppClassLoader 加载,需要反向委托(通过Thread.setContextClassLoader()
)。
-
-
OSGi 模块化(如 Eclipse):每个模块有自己的类加载器,打破双亲委派,实现类隔离。
6. 面试高频问题
-
如何证明双亲委派?
System.out.println(String.class.getClassLoader()); // null(Bootstrap 加载) System.out.println(MyClass.class.getClassLoader()); // AppClassLoader
-
Tomcat 如何隔离不同 Web 应用的类?
-
每个 Web 应用有自己的
WebAppClassLoader
,优先加载自己的类,而不是委托给父加载器(防止类冲突)。
-
一句话总结:类加载器是 JVM 的“搬运工”,通过双亲委派模型确保类的唯一性和安全性,同时支持自定义扩展实现动态加载。
3.2、双亲委派机制
双亲委派模型(Parent Delegation Model)是 类加载器之间的一种协作规范,而不是一个简单的“父子继承”关系。它的核心目标只有两句话:
“先让爸爸找,爸爸找不到儿子再找”,
“绝对不允许任何人篡改核心类”。
1、加载顺序
代码: new com.example.User()
│
│ ① 应用类加载器(AppClassLoader)
│ 收到加载请求
│ ↓ ② 先“向上抛”
│ 平台类加载器(PlatformClassLoader)
│ ↓ ③ 继续向上抛
│ 启动类加载器(BootstrapClassLoader)
│ ↓ ④ 真正开始找
│ 在 rt.jar 里没找到 User.class
│ ↑ ⑤ 回到下一层
│ 平台类加载器也没找到
│ ↑ ⑥ 回到最下层
│ 应用类加载器最终在 classpath 里找到并加载
2. 一张表:三类加载器的行为差异
加载器类型 | 负责范围 | 是否遵守双亲委派 | 破坏委派的典型场景 |
---|---|---|---|
Bootstrap | java.* 、sun.* 等核心类 | 是(最顶层,没有父亲) | 无 |
Platform | $JAVA_HOME/lib/ext/*.jar | 是 | 无 |
Application | 业务代码、三方库 | 是 | SPI 反向委派(见下) |
自定义 | 热部署、加密类、网络类 | 可破坏 | 重写 loadClass() 跳过父类 |
3、破坏双亲委派的现实需求
-
JDBC 4.0:
java.sql.Driver
由 Bootstrap 加载,但实现类(如com.mysql.cj.jdbc.Driver
)在应用 jar 中。
→ 靠Thread.getContextClassLoader()
反向委派,让 Bootstrap 委托应用类加载器去加载 MySQL 驱动。 -
Tomcat 隔离:每个 Web 应用优先用自己的
WebAppClassLoader
加载类,如果先委派给父级就会和宿主 Tomcat 的类冲突。
4、代码验证
// 证明 java.lang.String 一定由 Bootstrap 加载
System.out.println(String.class.getClassLoader()); // null(Bootstrap 的标记)
// 证明业务类由 AppClassLoader 加载
System.out.println(MyClass.class.getClassLoader()); // sun.misc.Launcher$AppClassLoader@...
// 证明双亲委派:自定义类加载器加载 java.lang.String 会失败
try {
new MyClassLoader().loadClass("java.lang.String");
} catch (SecurityException e) {
System.out.println("禁止自定义类加载器覆盖核心类!");
}
5、记忆口诀
“爸爸没找过,儿子绝不碰;核心类防篡改,反向委派有特例。”
四、沙箱安全机制(具体可以参考java中的安全模型(沙箱机制)_propertypermission-CSDN博客)
Java 的 沙箱安全机制(Security Sandbox) 是一组层层设卡的“防护网”,让不可信的代码(如 Applet、用户上传的 JAR、第三方库)即使被 JVM 加载,也 只能在被批准的“安全领地”内活动,无法触碰宿主机关键资源。
核心思路:
“先验身、再授权、后审计,全程可撤销。”
1、四层防护结构(从外到内)
层级 | 机制 | 作用 | 典型配置/代码 |
---|---|---|---|
① 类加载器 | 双亲委派 + 命名空间隔离 | 防止类伪造(如自定义 java.lang.String ) | 自定义 ClassLoader 重写 findClass |
② 字节码校验器 | 静态校验 + 运行时检查 | 防止非法字节码(栈溢出、类型转换错误) | JVM 自动执行,-Xverify:none 可关闭(危险) |
③ 安全管理器(SecurityManager) | 运行时权限检查 | 文件/网络/反射等敏感操作需授权 | System.setSecurityManager(new SecurityManager()) |
④ 访问控制器(AccessController) | 基于策略的细粒度权限 | 代码签名 + 策略文件 (*.policy ) | grant codeBase "file:/app/*" { permission java.io.FilePermission "/tmp/-", "read,write"; }; |
2、实战:限制用户上传的 JAR 只能读 /tmp
// 1. 启动时加载策略文件
java -Djava.security.manager -Djava.security.policy=my.policy -jar user-upload.jar
// 2. my.policy 文件内容
grant codeBase "file:/uploads/user-code.jar" {
permission java.io.FilePermission "/tmp/-", "read,write";
permission java.lang.RuntimePermission "getFileSystemAttributes";
// 禁止网络、系统属性、反射等
};
3、现代演进(Java 9+ 模块化)
-
模块系统(JPMS):
在module-info.java
中声明 显式导出/开放包,取代部分安全管理器职责:module com.example.app { requires java.base; exports com.example.api; // 其他模块只能访问导出的包 opens com.example.internal to trusted.module; // 反射开放 }
-
JEP 411:SecurityManager 已标记 弃用,未来沙箱更多依赖 容器/操作系统级隔离(如 Docker、Seccomp)。
4、一句话总结
Java 沙箱 = 类加载隔离 + 字节码校验 + 策略授权 + 审计追踪,让 不可信代码只能做“被允许的最小操作”,即使漏洞出现也能把损失锁在笼子里。
五、native关键字以及方法区
5.1、native关键字
在 Java 中,native
是一个 关键字,用于声明一个方法的实现是由 本地代码(通常是 C 或 C++) 提供的,而不是由 Java 代码实现的。这些方法被称为 本地方法(Native Methods)。在JVM中,它们会被调入到本地方法栈中,通过本地方法接口,查找本地方法库,去执行本地方法。
✅ 使用场景
-
需要调用 操作系统底层 API(如文件系统、硬件访问)。
-
使用 已有的 C/C++ 库(如 OpenCV、TensorFlow 的 C++ 接口)。
-
对性能要求极高的代码,用 C/C++ 实现更高效。
✅ 基本语法
public class NativeExample {
// 声明一个本地方法
public native void sayHello();
public static void main(String[] args) {
// 加载本地库
System.loadLibrary("NativeLib");
new NativeExample().sayHello();
}
}
✅ 实现步骤(JNI 流程)
-
声明 native 方法(如上)。
-
生成头文件(使用
javah
或javac -h
javac NativeExample.java javah -jni NativeExample
会生成
NativeExample.h
。 -
实现 C/C++ 代码(实现头文件中的函数)
#include <jni.h> #include "NativeExample.h" #include <stdio.h> JNIEXPORT void JNICALL Java_NativeExample_sayHello(JNIEnv *env, jobject obj) { printf("Hello from C!\n"); }
-
编译为共享库(Linux/macOS 用
.so
,Windows 用.dll
)gcc -shared -fPIC -o libNativeLib.so NativeExample.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux"
-
运行 Java 程序(确保库路径正确)
java -Djava.library.path=. NativeExample
✅ 注意事项
-
库名匹配:
System.loadLibrary("NativeLib")
会加载libNativeLib.so
(Linux)或NativeLib.dll
(Windows)。 -
线程安全:本地代码中需要手动处理线程安全问题。
-
异常处理:本地代码崩溃会导致 JVM 崩溃(
Fatal error
),需谨慎。
✅ 示例项目结构
project/
├── NativeExample.java
├── NativeExample.h
├── NativeExample.c
├── libNativeLib.so
✅ 总结
特性 | 描述 |
---|---|
关键字 | native |
实现语言 | C/C++(或其他本地语言) |
加载方式 | System.loadLibrary() |
技术名称 | JNI(Java Native Interface) |
5.2、方法区(存储stastic、final、类模板、常量池...)
方法区(Method Area)是 JVM 规范中定义的 逻辑区域,用于存储 类元数据 和 运行时常量池。
从 JDK 8 开始,HotSpot 用 Metaspace 实现方法区,并把它从 永久代(PermGen) 移到了 本地内存(Native Memory)。
✅ 一句话定义
方法区是 JVM 为 每个类 保存“静态结构”的地方,包括类信息、常量、静态变量、即时编译后的代码等。
✅ 存储内容(JDK 8+)
内容项 | 说明 |
---|---|
类元信息 | 类的全限定名、父类、接口、字段、方法、访问标志(如 ACC_PUBLIC )等 |
运行时常量池 | 字面量(字符串、数字)、符号引用(类名、方法名、字段描述符) |
静态变量 | static 修饰的字段(类变量) |
JIT 编译后的代码 | HotSpot 的 Code Cache(位于本地内存,但逻辑上属于方法区) |
运行时常量池 | 类加载后,class 文件中的常量池被解析为运行时常量池 |
✅ 实现演进
JDK 版本 | 实现方式 | 内存位置 | 溢出异常 |
---|---|---|---|
≤ JDK 7 | 永久代(PermGen) | JVM 堆内存 | java.lang.OutOfMemoryError: PermGen space |
≥ JDK 8 | Metaspace | 本地内存 | java.lang.OutOfMemoryError: Metaspace |
✅ 与 Native 关键字的关系
-
类加载 时,本地方法的
ACC_NATIVE
标记存储在 方法区。 -
JNI 调用 时,JVM 从方法区获取
jclass
/jmethodID
。
✅ 常见误区
-
❌ “方法区 = 堆” → 错误,方法区是逻辑区域,JDK 8 后物理上在 本地内存。
-
❌ “静态变量在堆” → 部分正确,静态变量在方法区(Metaspace),但对象实例在堆。
✅ 示例
public class Example {
static int count = 0; // 存储在方法区(Metaspace)
native void callC(); // 方法的元信息(ACC_NATIVE)也在方法区
}
✅ 总结
特性 | 描述 |
---|---|
作用 | 存储类结构、常量、静态数据 |
位置 | JDK 8 前:永久代(堆内) |
JDK 8 后:Metaspace(本地内存) | |
GC | 类卸载时回收(条件严格) |
OOM | OutOfMemoryError: Metaspace |
方法区就像 JVM 的“类仓库”,而 Metaspace 是它在新版本中的具体实现。
六、栈
6.1、栈
在 JVM 中,“栈”通常指 Java 虚拟机栈(JVM Stack),它与 本地方法栈(Native Method Stack) 是两块不同的内存区域。
名称 | 用途 | 线程私有? | 溢出异常 |
---|---|---|---|
Java 虚拟机栈 | 执行 Java 方法(字节码) | 是 | StackOverflowError / OutOfMemoryError |
本地方法栈 | 执行 native 方法(C/C++) | 是 | 同上 |
C 运行时栈 | 操作系统层面的线程栈 | 是 | 由 OS 决定(段错误等) |
✅ Java 虚拟机栈(JVM Stack)细节
1. 组成:由 栈帧(Stack Frame) 构成
每调一次 Java 方法就压入一个栈帧,方法返回后弹出。
2. 栈帧内部结构(HotSpot)
+-----------------------------+
| 局部变量表(Local Variable Array)| 保存参数、局部变量
| 操作数栈(Operand Stack) | 字节码指令的工作区
| 动态链接(Dynamic Linking) | 指向运行时常量池的方法引用
| 方法返回地址(Return Address) | 调用者 PC
| 附加信息(调试、锁记录等) |
+-----------------------------+
3. 生命周期
-
线程创建时 JVM 为其分配一块栈内存(默认 1 MB,可
-Xss
调整)。 -
方法调用时 压帧;方法返回 时弹帧;异常抛出 时做栈展开(stack unwinding)。
4. 常见异常
-
StackOverflowError:递归太深,栈帧数 > 栈深度。
-
OutOfMemoryError:线程太多,无法为新线程申请栈内存。
✅ 本地方法栈(Native Method Stack)
-
当 Java 调用
native
方法时,JVM 通过 JNI wrapper 进入 C/C++,本地方法栈 就是 C/C++ 使用的栈。 -
HotSpot 把 Java 栈 与 本地方法栈 合并在同一块物理内存(同一条线程栈),因此:
-
Java → native:C 帧直接长在 Java 帧之上。
-
native → Java:通过
JNIEnv
回调 Java 方法时,会再压入新的 Java 帧。
-
✅ 示例:一次跨栈调用
class Demo {
public static void main(String[] a) { foo(); }
static void foo() { bar(); }
static native void bar(); // C 实现
}
运行时栈(从底向上):
| 线程启动帧 | ← OS 分配栈底
| main() Java 帧 |
| foo() Java 帧 |
| bar() JNI stub 帧 | ← Java→native 边界
| bar() C 帧 | ← 本地方法栈
✅ 调优与诊断
选项/工具 | 作用示例 |
---|---|
-Xss1m | 设置每条线程 JVM 栈大小 |
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps | 观察线程创建失败时的 OOM |
jstack <pid> | 打印所有线程的栈帧(Java + JNI) |
jcmd <pid> Thread.print | 同上,支持更多诊断信息 |
✅ 一句话总结
Java 栈 存 Java 方法帧,本地方法栈 存 native 方法帧;HotSpot 把两者放在同一条 OS 线程栈 上,只是视角不同。
6.2、Java对象在内存中的实例化的过程
6.3、三种JVM(学的都是基于HotSpot)
-
HotSpot(Oracle/OpenJDK 默认):最常用,JIT、GC、Profiler 最全。
-
OpenJ9(IBM/Eclipse): 启动快、内存省,适合云/容器。
-
GraalVM(Oracle Labs):多语言(Java/JS/Python/R),AOT 原生镜像(Spring Native 就靠它)。
-
总而言之,言而总之,日常开发默认 HotSpot;想省内存用 OpenJ9;要原生或混语言用 GraalVM。
七、堆
堆是 JVM 里最大的一块内存,唯一目的就是“放对象”——所有 new
出来的实例、数组都在这儿,由 GC 统一管理。
八、新生代、老年代、永久代、堆内存调优
九、使用Jpofiler工具分析OOM原因
使用JPofiler工具分析OOM原因_oom问题结合jprofiler排查-CSDN博客
jvm调优命令
十、GC(垃圾回收机制)
10.1、GC所管理的区域
10.2、GC算法
1. 引用计数(Reference Counting)
-
原理:每个对象维护一个引用计数器,当引用增加(如赋值)时计数器+1,引用失效(如变量离开作用域)时-1。计数器为0时立即回收。
-
优点:实时回收,无暂停(无STW,Stop-The-World)。
-
缺点:无法处理循环引用(如A→B→A),计数器更新开销大。
-
应用:Python(CPython辅助使用)、Objective-C(ARC优化)。
2. 标记-清除(Mark-Sweep)
-
原理:
-
标记阶段:从根(GC Roots,如全局变量、栈中引用)出发,遍历所有可达对象并标记。
-
清除阶段:回收未标记的对象。
-
-
优点:解决循环引用,算法简单。
-
缺点:产生内存碎片,清除阶段可能暂停应用(STW)。
-
应用:早期JVM(已淘汰)、Lua。
3. 标记-整理(Mark-Compact)
-
原理:在标记-清除基础上,将存活对象向内存一端移动,消除碎片后清理边界外内存。
-
优点:无碎片,适合老年代(长期存活对象)。
-
缺点:对象移动成本高,暂停时间较长。
-
应用:JVM老年代(Serial Old、Parallel Old收集器)。
4. 复制算法(Copying)
-
原理:将内存分为两块(From/To),存活对象从From复制到To后交换角色,直接清理整块From。
-
优点:无碎片,复制高效(适合短生命周期对象)。
-
缺点:内存利用率低(50%),复制长存活对象成本高。
-
应用:JVM新生代(Serial、ParNew收集器的Eden/Survivor区)。
5. 分代收集(Generational Collection)
-
原理:基于“弱分代假说”(多数对象朝生夕死),将堆分为新生代(复制算法)和老年代(标记-清除/整理),分别优化。
-
优化点:新生代频繁GC(Minor GC),老年代低频Full GC。
-
应用:现代JVM(G1、ZGC、Shenandoah)、.NET CLR。
6. 增量收集(Incremental GC)
-
原理:将GC过程拆分为多个小步骤,与应用线程交替执行,减少单次暂停时间。
-
挑战:需解决GC过程中对象关系变化(如写屏障Write Barrier)。
-
应用:V8引擎(早期)、部分实时系统。
7. 并发/并行收集(Concurrent/Parallel GC)
-
并发(Concurrent):GC线程与应用线程同时运行(如CMS、G1的并发标记)。
-
并行(Parallel):多GC线程并行加速回收(如Parallel Scavenge)。
-
应用:
-
CMS(Concurrent Mark-Sweep):低延迟,但产生碎片。
-
G1(Garbage First):区域化分代+并发标记+整理,平衡吞吐与延迟。
-
ZGC/Shenandoah:亚毫秒级暂停,支持TB级堆(JDK11+)。
-
8. 引用类型优化
-
强/软/弱/虚引用:允许开发者控制对象生命周期(如Java的
SoftReference
缓存、WeakHashMap)。
对比表
算法 | 内存碎片 | 暂停时间 | 吞吐量 | 适用场景 |
---|---|---|---|---|
引用计数 | 无 | 极低 | 低 | 实时系统(无循环引用) |
标记-清除 | 有 | 中 | 中 | 老年代(低频GC) |
标记-整理 | 无 | 高 | 中 | 老年代(大对象) |
复制算法 | 无 | 低 | 高 | 新生代(小对象) |
G1/ZGC | 无/低 | 极低 | 高 | 大堆、低延迟应用 |
十一、JMM
JMM(Java Memory Model,Java内存模型) 是Java虚拟机规范中定义的多线程内存访问规则,用于解决并发编程中的可见性、有序性、原子性问题。它屏蔽了不同硬件/操作系统的内存差异,确保多线程程序在不同平台下行为一致。
1. 核心目标
-
可见性:一个线程对共享变量的修改,其他线程能否立即看到。
-
有序性:禁止指令重排序(编译器/CPU优化)导致的执行顺序问题。
-
原子性:保证基本操作的不可分割性(如
long
/double
以外的读写)。
2. 主内存与工作内存
-
主内存(Main Memory):所有共享变量的存储区域(物理内存或堆内存)。
-
工作内存(Working Memory):每个线程的私有内存(CPU缓存/寄存器),存储共享变量的副本。
-
交互规则:线程对变量的读写必须通过工作内存,不能直接操作主内存。
3. 内存间的交互操作(8种原子操作)
操作 | 作用(线程→内存) |
---|---|
lock | 将主内存变量标记为线程独占(锁)。 |
unlock | 释放锁,将变量写回主内存。 |
read | 从主内存读取变量值到工作内存。 |
load | 将read 的值放入工作内存的变量副本。 |
use | 将工作内存变量值传递给执行引擎。 |
assign | 将执行引擎结果赋值给工作内存变量。 |
store | 将工作内存变量值传回主内存。 |
write | 将store 的值写入主内存变量。 |
4. 三大特性规则
(1)可见性(Visibility)
-
问题:线程A修改变量后,线程B可能仍使用旧值(因工作内存未同步)。
-
解决:
-
volatile
:强制读写直接操作主内存,禁止缓存优化。 -
synchronized
:解锁前必须将工作内存变化刷新到主内存。 -
final
:初始化完成后不可变,其他线程可见。
-
(2)有序性(Ordering)
-
问题:编译器/CPU可能重排序指令(如
a=1; b=2;
可能先执行b=2
)。 -
解决:
-
volatile
:禁止指令重排序(通过内存屏障)。 -
synchronized
:同一锁的代码块内保持串行执行。
-
(3)原子性(Atomicity)
-
基本类型:除
long
/double
外,读写天然原子(32位JVM需volatile
保证64位原子性)。 -
复合操作:需
synchronized
或AtomicInteger
等原子类。
5. Happens-Before规则(先行发生原则)
JMM定义的天然有序性关系,无需同步即可保证可见性:
-
程序顺序规则:单线程内,代码顺序执行。
-
锁规则:解锁
unlock
先于后续加锁lock
。 -
volatile规则:写操作先于读操作。
-
线程启动/终止:
Thread.start()
先于线程内操作,线程内操作先于Thread.join()
返回。 -
传递性:若A→B,B→C,则A→C。
6. 示例:volatile的可见性
class Example {
volatile boolean flag = false;
void writer() {
flag = true; // 写操作立即刷回主内存
}
void reader() {
if (flag) { // 读操作直接从主内存获取
System.out.println("可见");
}
}
}
-
若
flag
无volatile
,线程B可能永远看不到true
。
7. JMM vs 运行时内存区域
-
JMM:逻辑模型,定义线程如何交互共享变量。
-
运行时内存区域(JVM内存结构):物理划分(堆、栈、方法区等)。
-
例如:工作内存可能对应CPU缓存,主内存对应堆。
-
8. 常见误区
-
volatile
不能保证原子性:如volatile i++
仍非线程安全(需AtomicInteger
)。 -
synchronized
不仅保证原子性:也确保可见性(解锁时刷新内存)。
总结
JMM通过抽象的主内存/工作内存模型和Happens-Before规则,屏蔽底层硬件差异,使开发者无需关心缓存一致性,只需正确使用volatile
、synchronized
等关键字即可编写线程安全代码。