文章目录
- 第一部分:JVM基础概念与架构
- JVM是什么?
- JVM整体架构
- 运行时数据区
- 类加载机制
- 执行引擎
- 第二部分:Java内存模型(JMM)
- 什么是Java内存模型
- JMM的核心问题
- 主内存与工作内存
- 内存间交互操作
- 重排序与happens-before原则
- volatile关键字
- synchronized关键字
- 第三部分:Java 8到Java 21的JVM和JMM演进
- Java 8到Java 11的变化
- JVM架构变化
- JMM相关变化
- Java 11到Java 17的变化
- JVM架构变化
- JMM相关变化
- Java 17到Java 21的变化
- JVM架构变化
- JMM相关结构化并发
- 第四部分:垃圾回收器详解
- G1垃圾回收器
- 核心特性
- G1的工作流程
- 适用场景
- 调优参数
- ZGC垃圾回收器
- 核心特性
- ZGC的工作流程
- 适用场景
- 调优参数
- Shenandoah垃圾回收器
- 核心特性
- Shenandoah的工作流程
- 适用场景
- 调优参数
- 垃圾回收器性能对比
- 停顿时间对比
- 吞吐量对比
- 内存开销对比
- 第五部分:虚拟线程与结构化并发
- 虚拟线程简介
- 虚拟线程与平台线程的区别
- 虚拟线程的使用场景
- 虚拟线程的使用示例
- 结构化并发API
- 核心概念
- 结构化并发的优势
- 结构化并发的使用示例
- 第六部分:面试常见问题与回答
- Q1: G1、ZGC和Shenandoah的主要区别是什么?
- Q2: 为什么ZGC能实现低于10ms的停顿时间?
- Q3: G1收集器的"可预测停顿"是如何实现的?
- Q4: Java 17相比Java 8在GC方面有哪些重要改进?
- Q5: 什么是双亲委派模型?如何破坏双亲委派模型?
- Q6: 什么是JIT编译器?它如何提高Java程序的性能?
- Q7: Java内存模型中的happens-before原则是什么?
- Q8: 虚拟线程与传统线程的区别是什么?
- Q9: 元空间与永久代的区别是什么?
- Q10: 如何选择合适的垃圾回收器?
- 总结
- 参考资料
在Java开发者的职业生涯中,理解JVM(Java虚拟机)和JMM(Java内存模型)是进阶的必经之路。无论是日常开发还是面试,这都是绕不开的话题。本文将从实用角度出发,带你深入了解JVM和JMM的核心概念,并对比Java 8、Java 11、Java 17和Java 21各版本的实现差异,帮助大家更好的了解JVM和JMM
第一部分:JVM基础概念与架构
JVM是什么?
简单来说,JVM是一个抽象的计算机,它提供了一个平台无关的执行环境,让Java程序可以"一次编写,到处运行"。当我们编写Java代码时,编译器会将源代码(.java文件)编译成字节码(.class文件),而JVM则负责解释执行这些字节码。
这种设计带来了两个关键优势:
- 平台独立性:Java程序可以在任何安装了JVM的设备上运行
- 安全性:JVM提供了一个受控的执行环境,限制了程序对系统资源的直接访问
JVM整体架构
JVM的架构可以分为三大部分:
- 类加载子系统:负责加载、链接和初始化类
- 运行时数据区:JVM内存模型,包括方法区、堆、Java栈、本地方法栈和程序计数器
- 执行引擎:包括即时编译器(JIT)、解释器和垃圾回收器
运行时数据区
JVM的内存区域划分是面试的高频考点,包括:
-
程序计数器:
- 当前线程执行的字节码指令地址
- 线程私有,是唯一不会发生OOM的内存区域
-
Java虚拟机栈:
- 线程私有,生命周期与线程相同
- 每个方法执行时会创建一个栈帧,包含局部变量表、操作数栈、动态链接和方法出口等信息
- 可能抛出StackOverflowError和OutOfMemoryError
-
本地方法栈:
- 为本地(Native)方法服务
- 也可能抛出StackOverflowError和OutOfMemoryError
-
Java堆:
- 线程共享,存储对象实例
- 垃圾收集器管理的主要区域,也称为"GC堆"
- 可能抛出OutOfMemoryError
-
方法区:
- 存储已被虚拟机加载的类信息、常量、静态变量等
- 在HotSpot虚拟机中,JDK8前称为"永久代",JDK8后称为"元空间"
- 可能抛出OutOfMemoryError
类加载机制
类加载过程分为三个阶段:
- 加载:查找并加载类的二进制数据
- 链接:
- 验证:确保加载的类符合JVM规范
- 准备:为类的静态变量分配内存并设置初始值
- 解析:将符号引用转换为直接引用
- 初始化:执行类构造器
<clinit>()
方法
类加载器遵循三个重要原则:
- 委托机制:先让父加载器尝试加载
- 可见性:子加载器可以看到父加载器加载的类,反之不行
- 唯一性:同一个类(由类名和加载器共同决定)只会被加载一次
JVM提供了三种内置的类加载器:
-
启动类加载器(Bootstrap ClassLoader):
- 负责加载Java核心类库
- 由C++实现,是JVM的一部分
-
扩展类加载器(Extension ClassLoader):
- 负责加载Java扩展类库
- JDK 9后改名为平台类加载器(Platform ClassLoader)
-
应用类加载器(Application ClassLoader):
- 负责加载应用程序classpath下的类
执行引擎
执行引擎是JVM的核心组件,负责执行字节码指令:
- 解释器:逐条解释执行字节码指令
- JIT编译器:将热点代码编译成本地机器码,提高执行效率
- 垃圾回收器:自动回收不再使用的内存
JVM采用混合模式执行:
- 解释执行:启动快,执行慢
- 编译执行:启动慢,执行快
通过热点代码探测,JVM能够在解释执行和编译执行之间取得平衡,实现"解释+编译"的混合执行模式。
第二部分:Java内存模型(JMM)
什么是Java内存模型
Java内存模型(Java Memory Model,简称JMM)是Java虚拟机规范中定义的一种内存模型,用于屏蔽各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到一致的内存访问效果。
JMM不是真实存在的物理内存区域,而是一种抽象概念,是一组规范,定义了程序中各个变量的访问方式。
JMM的核心问题
JMM主要解决了三个核心问题:
- 可见性:一个线程对共享变量的修改,能够及时被其他线程看到
- 原子性:一个操作或多个操作要么全部执行并且不会被中断,要么都不执行
- 有序性:程序执行的顺序按照代码的先后顺序执行
主内存与工作内存
在JMM中,所有的变量都存储在主内存(Main Memory)中,每个线程还有自己的工作内存(Working Memory):
- 主内存:所有线程共享的内存区域,存储所有变量的主副本
- 工作内存:每个线程私有的内存区域,存储该线程使用到的变量的副本
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
内存间交互操作
JMM定义了8种操作来完成主内存和工作内存的交互:
- lock(锁定):作用于主内存的变量,把变量标识为一条线程独占状态
- unlock(解锁):作用于主内存的变量,释放变量的锁定状态
- read(读取):作用于主内存的变量,把变量的值从主内存传输到线程的工作内存
- load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,把执行引擎接收到的值赋给工作内存的变量
- store(存储):作用于工作内存的变量,把工作内存中的变量值传送到主内存中
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中
重排序与happens-before原则
为了提高性能,编译器和处理器常常会对指令进行重排序,分为三种类型:
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行
- 内存系统重排序:由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行
这些重排序可能导致多线程程序出现内存可见性问题。为此,JMM定义了happens-before原则,用于描述操作之间的内存可见性。如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作可见。
主要的happens-before规则包括:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- volatile变量规则:对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
- 线程启动规则:Thread对象的start()方法happens-before于该线程的任何动作
- 线程终止规则:线程中的所有操作都happens-before于其他线程检测到该线程已经终止
volatile关键字
volatile是Java提供的最轻量级的同步机制,它保证了:
- 可见性:对一个volatile变量的写,对其他线程立即可见
- 有序性:禁止指令重排序优化
volatile的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,从主内存中读取共享变量
volatile适用于一写多读的场景,但不保证原子性。例如,volatile++操作不是原子的,因为它包含读取、计算、写入三个步骤。
synchronized关键字
synchronized是Java中的重量级锁,它可以保证被修饰的代码块或方法在同一时刻只能被一个线程执行,从而保证了:
- 原子性:确保多个操作作为一个整体不可分割
- 可见性:synchronized的可见性是由"对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)"这条规则获得的
- 有序性:synchronized保证了一个变量在同一个时刻只能由一个线程对其进行lock操作
synchronized的内存语义:
- 进入synchronized块时,会清空工作内存中的共享变量值,从主内存中重新读取
- 退出synchronized块时,会将工作内存中的共享变量值刷新到主内存中
第三部分:Java 8到Java 21的JVM和JMM演进
Java 8到Java 11的变化
JVM架构变化
-
永久代到元空间的转变
- Java 8 将永久代(PermGen)完全移除,取而代之的是元空间(Metaspace)
- 元空间使用本地内存而非JVM堆内存,默认情况下可以动态增长,减少了"java.lang.OutOfMemoryError: PermGen space"错误
- 类的元数据信息放到本地内存,常量池和静态变量放到Java堆中
-
默认垃圾收集器变更
- Java 8 默认使用Parallel垃圾收集器
- Java 11 默认使用G1垃圾收集器,提供更好的延迟可预测性和更高的吞吐量
-
G1收集器的改进
- Java 11 中G1收集器实现了并行Full GC,大幅提高了Full GC的性能
- 引入了基于NUMA感知的内存分配优化
-
统一JVM日志系统
- Java 9 引入,Java 11 完善了统一的JVM日志系统
- 提供了一致的命令行选项和日志输出格式,便于问题诊断
JMM相关变化
-
变量句柄(VarHandle)
- Java 9 引入,Java 11 完善
- 提供比反射更精细的内存访问控制,支持原子操作和内存屏障
- 可以替代Unsafe类的部分功能,提供类型安全的方式访问对象字段和数组元素
-
内存管理优化
- Java 10 开始,JVM能够识别容器的内存限制(如Docker容器)
- 改进了内存分配策略,更好地适应现代云环境
Java 11到Java 17的变化
JVM架构变化
-
ZGC的引入与改进
- Java 11 引入ZGC作为实验性功能
- Java 15 将ZGC从实验特性转为产品特性
- Java 17 进一步优化ZGC,提供更低的延迟和更高的吞吐量
- ZGC支持并发类卸载、并发堆收缩,大幅减少停顿时间
-
Shenandoah GC
- Java 12 引入Shenandoah GC作为实验性功能
- Java 15 将Shenandoah GC从实验特性转为产品特性
- 专注于低停顿时间,适用于对响应时间敏感的应用
-
CMS收集器的移除
- Java 14 正式移除了CMS(Concurrent Mark Sweep)垃圾收集器
-
弹性元空间
- Java 16 引入弹性元空间能力
- 更高效地将未使用的HotSpot类元数据(即元空间)内存返回给操作系统
- 减少了元空间内存占用,提高了内存利用率
JMM相关变化
-
增强的同步机制
- Java 15 引入了改进的偏向锁实现
- Java 16 开始弃用偏向锁,并在Java 18中完全移除
-
内存管理优化
- 改进了G1和ZGC的内存回收策略
- 优化了对大对象和长寿命对象的处理
Java 17到Java 21的变化
JVM架构变化
-
虚拟线程的引入
- Java 19 引入虚拟线程作为预览特性
- Java 21 正式发布虚拟线程
- 虚拟线程是轻量级线程实现,由JVM而不是操作系统管理
- 支持高吞吐量的并发应用,可以创建数百万个虚拟线程
-
GC改进
- 进一步优化ZGC和G1收集器
- 改进了垃圾收集器的并行和并发能力
- 降低了Full GC的频率和停顿时间
-
JIT编译器优化
- 改进了即时编译器的优化策略
- 增强了逃逸分析和内联优化
- 提高了代码执行效率
JMM相关结构化并发
-
结构化并发API
- Java 19 引入结构化并发API作为预览特性
- Java 21 继续改进结构化并发API
- 提供了更简单、更可靠的并发编程模型
- 更好地支持异步编程和错误处理
-
外部内存访问API
- Java 14 引入外部内存访问API作为孵化器特性
- Java 21 将其作为正式特性发布
- 允许Java程序安全高效地访问Java堆外的内存
第四部分:垃圾回收器详解
G1垃圾回收器
G1(Garbage-First)是一种面向服务端应用的垃圾回收器,目标是替代CMS。
核心特性
- 分区(Region)设计:将堆内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor、Old或Humongous区域
- 可预测的停顿时间:通过
-XX:MaxGCPauseMillis
参数设置目标停顿时间 - 混合收集:既有年轻代收集,也有老年代收集
- 增量收集:不需要一次性收集整个堆,而是选择部分区域进行收集
G1的工作流程
- 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,需要STW(Stop The World)
- 并发标记(Concurrent Marking):对堆中对象进行可达性分析,与应用线程并发执行
- 最终标记(Final Marking):处理并发标记阶段遗留的少量SATB(Snapshot At The Beginning)记录,需要STW
- 筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划,需要STW
适用场景
- 需要较低GC延迟的大型应用(堆内存4GB-32GB)
- 电商、金融等对响应时间敏感的在线业务系统
- 多核CPU环境
调优参数
-XX:+UseG1GC // 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200 // 设置目标最大停顿时间为200毫秒
-XX:InitiatingHeapOccupancyPercent=45 // 设置触发标记周期的堆占用率阈值
-XX:G1NewSizePercent=5 // 设置年轻代最小占堆的百分比
-XX:G1MaxNewSizePercent=60 // 设置年轻代最大占堆的百分比
ZGC垃圾回收器
ZGC(Z Garbage Collector)是一款低延迟垃圾收集器,目标是在任何堆大小下都能将停顿时间控制在10ms以内。
核心特性
- 并发处理:几乎所有GC操作都与应用线程并发执行,包括标记、转移和重定位
- 基于区域的内存管理:类似G1,但区域大小动态可变(2MB-4TB)
- 标记-复制算法:使用复制算法进行内存整理
- 颜色指针:利用64位指针中的一些位作为标记位,实现快速对象定位和并发转移
- 支持超大堆:最高支持16TB的堆内存
ZGC的工作流程
- 并发标记:标记存活对象,与应用线程并发执行
- 并发转移准备:选择要清理的区域,与应用线程并发执行
- 并发转移:将存活对象从选定区域复制到新区域,与应用线程并发执行
- 并发重映射:更新引用,指向对象的新位置,与应用线程并发执行
适用场景
- 对延迟极为敏感的应用(如金融交易、游戏服务器)
- 大内存应用(堆内存>32GB)
- 需要稳定响应时间的实时系统
调优参数
-XX:+UseZGC // 使用ZGC垃圾收集器
-XX:ZAllocationSpikeTolerance=2 // 设置内存分配峰值容忍度
-XX:+UnlockExperimentalVMOptions // 解锁实验性VM选项(Java 15之前需要)
-XX:ConcGCThreads=2 // 设置并发GC线程数
Shenandoah垃圾回收器
Shenandoah是一款低延迟垃圾收集器,与ZGC类似,但实现方式不同。
核心特性
- 并发整理:与ZGC类似,支持并发标记和并发整理
- Brooks转发指针:每个对象都有一个额外的引用字段,指向对象的当前位置
- 不分代:默认不使用分代收集
- 连接矩阵:替代G1的记忆集,降低内存开销
Shenandoah的工作流程
- 初始标记:标记GC Roots直接引用的对象,需要STW
- 并发标记:递归标记整个对象图,与应用线程并发执行
- 最终标记:处理剩余的SATB缓冲区,需要STW
- 并发清理:回收确定为垃圾的区域,与应用线程并发执行
- 并发整理:将存活对象复制到新的内存区域,与应用线程并发执行
适用场景
- 需要低延迟但内存不是特别大的应用(堆内存8GB-32GB)
- 对延迟敏感的Web应用和服务
- 混合工作负载的应用
调优参数
-XX:+UseShenandoahGC // 使用Shenandoah垃圾收集器
-XX:ShenandoahGCHeuristics=adaptive // 设置启发式策略
-XX:+UnlockExperimentalVMOptions // 解锁实验性VM选项(Java 15之前需要)
-XX:ConcGCThreads=2 // 设置并发GC线程数
垃圾回收器性能对比
停顿时间对比
- Parallel GC:百毫秒级,与堆大小成正比
- G1 GC:目标通常为100-200毫秒,但可能出现偶发的长时间停顿
- Shenandoah:通常在10-100毫秒之间,与堆大小关系较小
- ZGC:通常小于10毫秒,几乎与堆大小无关
吞吐量对比
- Parallel GC:最高(约99%)
- G1 GC:中高(约95-98%)
- Shenandoah:中等(约90-95%)
- ZGC:中等(约90-95%),但在Java 17后有显著提升
内存开销对比
- Parallel GC:最低
- G1 GC:中等(记忆集占用额外内存)
- Shenandoah:中高(Brooks转发指针占用额外内存)
- ZGC:中高(颜色指针机制占用额外内存)
更详细的介绍可以参考另一篇博文
第五部分:虚拟线程与结构化并发
虚拟线程简介
虚拟线程是Java 21引入的重要特性,它是一种轻量级线程实现,由JVM而不是操作系统管理。虚拟线程的设计目标是提高应用程序的吞吐量,特别是在处理大量并发任务时。
虚拟线程与平台线程的区别
- 创建成本:虚拟线程的创建成本远低于平台线程
- 内存占用:虚拟线程的内存占用很小,可以创建数百万个
- 调度方式:虚拟线程由JVM调度,而平台线程由操作系统调度
- 阻塞行为:虚拟线程阻塞时不会阻塞底层的平台线程
虚拟线程的使用场景
- 高并发服务器:处理大量并发连接
- 微服务架构:每个请求使用一个虚拟线程处理
- 异步编程:简化异步编程模型,使代码更易于理解和维护
虚拟线程的使用示例
// 创建并启动一个虚拟线程
Thread vThread = Thread.startVirtualThread(() -> {System.out.println("Running in a virtual thread");
});// 使用虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(0, 10_000).forEach(i -> {executor.submit(() -> {// 任务代码return i;});});
}
结构化并发API
结构化并发API是Java 19引入的预览特性,在Java 21中继续改进。它提供了一种更简单、更可靠的并发编程模型,特别适合与虚拟线程配合使用。
核心概念
- 结构化任务作用域:定义任务的生命周期和边界
- 子任务:在作用域内创建的任务
- 作用域关闭:确保所有子任务在作用域关闭前完成
结构化并发的优势
- 简化错误处理:子任务的异常会传播到父作用域
- 自动取消:当一个子任务失败时,其他子任务会被自动取消
- 避免资源泄漏:确保所有子任务在作用域关闭前完成或取消
- 提高代码可读性:使并发代码的结构更加清晰
结构化并发的使用示例
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {Future<String> user = scope.fork(() -> findUser(userId));Future<Integer> order = scope.fork(() -> fetchOrder(orderId));scope.join(); // 等待所有任务完成scope.throwIfFailed(); // 如果有任务失败,抛出异常// 使用结果processUserOrder(user.resultNow(), order.resultNow());
}
第六部分:面试常见问题与回答
Q1: G1、ZGC和Shenandoah的主要区别是什么?
回答:
- G1采用分区设计,兼顾吞吐量和停顿时间,适合中大型堆内存应用
- ZGC专注于极低延迟,使用颜色指针技术,适合大内存和对延迟极为敏感的应用
- Shenandoah也专注于低延迟,使用Brooks转发指针,适合中等内存和混合工作负载
Q2: 为什么ZGC能实现低于10ms的停顿时间?
回答:ZGC通过颜色指针技术和并发处理实现极低停顿。颜色指针利用64位指针中的一些位作为标记位,使得对象定位和转移能够并发执行,几乎所有GC操作(包括标记、转移和重定位)都与应用线程并发进行,只有极少数操作需要STW。
Q3: G1收集器的"可预测停顿"是如何实现的?
回答:G1通过以下机制实现可预测停顿:
- 将堆划分为多个大小相等的区域(Region)
- 维护每个区域的垃圾密度信息
- 根据用户设置的目标停顿时间(-XX:MaxGCPauseMillis)
- 在回收时优先选择回收价值最高(垃圾最多)的区域
- 动态调整年轻代大小和回收区域数量,以接近目标停顿时间
Q4: Java 17相比Java 8在GC方面有哪些重要改进?
回答:
- 默认GC从Parallel变为G1,更注重延迟而非单纯吞吐量
- 引入并完善了ZGC和Shenandoah两款低延迟收集器
- 移除了老旧的CMS收集器
- G1收集器得到多项优化,包括并行Full GC、NUMA感知等
- 引入弹性元空间,优化了元空间的内存管理
- 改进了GC的日志和监控能力
Q5: 什么是双亲委派模型?如何破坏双亲委派模型?
回答:双亲委派模型是指当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父加载器无法完成加载请求时,子加载器才会尝试自己加载。
破坏双亲委派模型的方式:
- 重写
loadClass()
方法:直接覆盖loadClass()
方法,不再调用父类加载器 - 使用线程上下文类加载器:通过
Thread.currentThread().getContextClassLoader()
获取上下文类加载器,打破了类加载器的层次结构 - 使用OSGi等模块化系统:OSGi实现了自己的类加载器架构,允许同一个类在不同的模块中加载
Q6: 什么是JIT编译器?它如何提高Java程序的性能?
回答:JIT(Just-In-Time)编译器是JVM的一个组件,它在运行时将热点代码(频繁执行的代码)编译成本地机器码,从而提高执行效率。
JIT编译器提高性能的方式:
- 热点代码识别:通过计数器或采样识别频繁执行的代码
- 即时编译:将字节码编译成优化的本地机器码
- 内联优化:将方法调用直接内联到调用点,减少方法调用开销
- 逃逸分析:分析对象的作用域,优化内存分配和同步
- 循环优化:提取循环不变量,减少计算量
- 分层编译:结合解释执行和不同级别的编译优化,平衡启动速度和峰值性能
Q7: Java内存模型中的happens-before原则是什么?
回答:happens-before是Java内存模型中的一个核心概念,用于描述操作之间的内存可见性。如果操作A happens-before操作B,那么A的结果对B可见。
主要的happens-before规则包括:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- volatile变量规则:对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读
- 线程启动规则:Thread对象的start()方法happens-before于该线程的任何动作
- 线程终止规则:线程中的所有操作都happens-before于其他线程检测到该线程已经终止
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
Q8: 虚拟线程与传统线程的区别是什么?
回答:
- 实现方式:虚拟线程由JVM实现和管理,传统线程(平台线程)由操作系统实现和管理
- 资源消耗:虚拟线程非常轻量,可以创建数百万个;平台线程较重,通常只能创建数千个
- 调度方式:虚拟线程使用协作式调度,平台线程使用抢占式调度
- 阻塞行为:虚拟线程阻塞时会让出底层的载体线程,而不会阻塞操作系统线程
- 栈大小:虚拟线程的栈可以动态增长,平台线程的栈大小是固定的
- 适用场景:虚拟线程适合IO密集型任务,平台线程适合CPU密集型任务
Q9: 元空间与永久代的区别是什么?
回答:
- 存储位置:永久代位于JVM堆内存中,而元空间使用本地内存(Native Memory)
- 内存限制:永久代有固定大小限制,元空间默认可以动态增长,只受系统内存限制
- 垃圾回收:元空间的垃圾回收效率更高,主要针对类元数据的回收
- OOM风险:永久代容易发生"PermGen space"的OOM错误,元空间降低了这种风险
- 内容存储:永久代存储类元数据、常量池和静态变量,元空间只存储类元数据,常量池和静态变量移到了堆中
Q10: 如何选择合适的垃圾回收器?
回答:选择垃圾回收器应考虑以下因素:
-
应用特性:
- 批处理、离线计算类应用:选择Parallel GC,获得最高吞吐量
- 一般在线业务系统:选择G1 GC,平衡吞吐量和延迟
- 对延迟敏感的在线服务:选择ZGC或Shenandoah,获得更低的延迟
-
内存大小:
- 小内存(<4GB):Parallel GC或G1 GC
- 中等内存(4GB-32GB):G1 GC或Shenandoah
- 大内存(>32GB):ZGC
-
Java版本:
- Java 8:Parallel GC(默认)或CMS
- Java 11:G1 GC(默认)或尝试实验性的ZGC
- Java 17及以上:G1 GC(默认)、ZGC或Shenandoah(均为产品特性)
总结
JVM和JMM是Java开发者必须掌握的核心知识,它们直接影响着Java程序的性能、稳定性和可靠性。从Java 8到Java 21,JVM和JMM经历了多次重要更新,包括永久代到元空间的转变、垃圾回收器的演进、虚拟线程的引入等。
在实际开发和面试中,理解这些概念和变化不仅有助于编写高质量的Java代码,还能帮助我们更好地进行性能调优和问题排查。希望本文能够帮助你在技术面试中脱颖而出,同时也能在日常工作中更加得心应手地处理JVM相关问题。
参考资料
- Oracle官方文档:JDK 11 Release Notes
- Oracle官方文档:JDK 17 Release Notes
- OpenJDK Wiki:ZGC
- Oracle官方文档:Java Language Changes
- 《深入理解Java虚拟机》第3版,周志明著
- 《Java并发编程的艺术》,方腾飞、魏鹏、程晓明著