Java 虚拟机 (JVM)
Java 虚拟机 (JVM) #
JVM 原理与调优面试核心知识点
📋 目录 #
内存模型 #
JVM 内存结构(Java 8+) #
| 区域 | 作用 | 线程私有 | 是否GC |
|---|---|---|---|
| 堆内存 | 存放对象实例 | ❌ | ✅ |
| 方法区 | 类信息、常量 | ❌ | ✅ |
| 虚拟机栈 | 方法调用、局部变量 | ✅ | ❌ |
| 本地方法栈 | Native方法 | ✅ | ❌ |
| 程序计数器 | 字节码行号 | ✅ | ❌ |
堆内存分代模型 #
比例: Eden : S0 : S1 = 8 : 1 : 1 | 配置:
-XX:SurvivorRatio=8
常用参数 #
# 堆内存设置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小
-Xmn2g # 年轻代大小
-XX:NewRatio=2 # 老年代:年轻代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 元空间(方法区)
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小
# 直接内存
-XX:MaxDirectMemorySize=1g
# 栈大小
-Xss512k # 线程栈大小
垃圾回收 #
对象存活判定 #
GC Roots 类型:
- 虚拟机栈中引用的对象
- 方法区静态变量引用
- 方法区常量引用
- 本地方法栈JNI引用
四种引用类型 #
| 类型 | 强度 | 回收时机 | 用途 |
|---|---|---|---|
| 强引用 | ★★★★★ | 不回收 | 普通对象引用 |
| 软引用 | ★★★★ | 内存不足 | 缓存 |
| 弱引用 | ★★★ | 下次GC | WeakHashMap |
| 虚引用 | ★ | GC时通知 | 对象回收跟踪 |
// 强引用
Object strong = new Object();
// 软引用
SoftReference<Object> soft = new SoftReference<>(new Object());
// 弱引用
WeakReference<Object> weak = new WeakReference<>(new Object());
// 虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
GC 算法对比 #
| 算法 | 收集区域 | 复制/标记方式 | 碎片 | 效率 |
|---|---|---|---|---|
| 标记-清除 | 老年代 | 标记 + 清除 | 有 ❌ | 低 |
| 标记-整理 | 老年代 | 标记 + 整理 | 无 ✅ | 中 |
| 复制算法 | 年轻代 | 复制到另一区 | 无 ✅ | 高 ⭐ |
垃圾收集器 #
垃圾收集器组合演变:
| 年轻代 | 老年代 | 适用场景 | JDK版本 |
|---|---|---|---|
| Serial | Serial Old | 单CPU,客户端 | 一直保留 |
| Parallel Scavenge | Parallel Old | 高吞吐量,后台任务 | JDK 8+ |
| ParNew | CMS | 低延迟,并发标记 | JDK 8-,JDK 9+移除CMS |
| G1 | G1 (分区) | 面向服务端,大堆 | JDK 9+ 默认 |
| ZGC | ZGC (整堆) | 超低延迟,超大堆 | JDK 15+ 正式 |
Serial 收集器(串行) #
实现原理:
- 单线程收集,暂停所有用户线程(Stop The World)
- 年轻代用复制算法,老年代用标记-整理
- 客户端模式默认收集器
优点:
- 简单高效,没有线程交互开销
- 单CPU场景比并发收集器更快
缺点:
- 收集时暂停所有线程,不适合服务端
Parallel 收集器(并行) #
实现原理:
- 多线程并行收集,仍然STW
- 年轻代 Parallel Scavenge(复制),老年代 Parallel Old(标记-整理)
- 目标是高吞吐量
参数:
-XX:+UseParallelGC
-XX:+UseParallelOldGC # JDK 8+ 默认开启
-XX:ParallelGCThreads=N # 并行线程数
适用场景:
- 多核CPU,注重吞吐量
- 后台计算、批处理任务
CMS 收集器(并发标记清除) #
目标: 低延迟,获取最短回收停顿时间
实现原理(四阶段):
STW 阶段:
- 初始标记:仅标记GC Roots能直接关联到的对象 → 快
- 重新标记:修正并发标记期间因用户程序运行导致标记变动 → 比初始标记稍长,远比对数整个堆扫描快
并发阶段:
- 并发标记:和用户线程一起并发遍历标记对象 → 慢但不暂停
- 并发清除:清除垃圾,和用户并发执行 → 慢但不暂停
特点:
- 整体是并发收集,停顿时间短
- 老年代用标记-清除算法,会产生内存碎片
- 需要预留空间给并发运行时分配新对象
缺点:
- 浮动垃圾:并发清除阶段用户程序还在分配新垃圾,只能下次GC回收
- 内存碎片:标记清除产生碎片,需要整理
- 对CPU敏感:并发阶段占用CPU时间
为什么CMS被移除?
- G1/ZGC性能更好,解决了CMS内存碎片问题
- JDK 14 正式移除 CMS
G1 收集器 Garbage-First #
面向服务端大堆,默认收集器(JDK 9+)⭐⭐⭐⭐⭐
核心设计思想 #
Region 分区:
- 整个堆分成多个等大 Region,每个 Region 大小 1MB ~ 32MB
- 不再固定区分 Eden/Survivor/Old,任何 Region 都可以存放
- 大对象(超过Region一半)直接放在连续Humongous Region
- 垃圾回收优先回收垃圾多的Region,所以叫 Garbage-First
回收过程(四种阶段) #
1. 年轻代 GC (Evacuation)
- Eden满了触发YGC
- 复制存活对象到Survivor Region
- STW 暂停,多线程并行复制
2. 并发标记
- 当整堆占用超过
InitiatingHeapOccupancyPercent(默认45%)触发 - 和应用线程并发标记,几乎不暂停
- 标记哪些Region有存活对象
3. 重新标记
- STW,修正并发标记期间的变动
- 处理漏标对象
4. 混合回收 Mixed GC
- 选择多个垃圾多收益高的Old Region + 年轻代一起回收
- 复制存活对象到新Region,腾出空间
- 复制过程=标记-整理,解决了CMS内存碎片问题
G1 关键特性 #
可预测停顿模型:
- 用户指定期望最大停顿时间
-XX:MaxGCPauseMillis=200 - G1 会调整回收Region数量,尽量满足停顿目标
- 牺牲一点吞吐量换可预测的低停顿
Remembered Set(记忆集):
- 每个Region有自己的Remembered Set
- 记录其他Region指向本Region的引用
- 扫描时只扫描Remembered Set,不用扫描全堆,加速YGC
特点总结:
| 特性 | 说明 |
|---|---|
| 分区 | 不固定分代,Region动态分配 |
| 可预测停顿 | 用户指定目标停顿,G1自动调整 |
| 标记-整理 | 复制整理,无内存碎片 |
| 并发标记 | 大部分标记和应用并发 |
| 适合场景 | 服务端大堆(4GB+),需要可控停顿 |
配置示例:
-XX:+UseG1GC # 启用G1
-XX:MaxGCPauseMillis=200 # 期望最大停顿毫秒
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率%
-XX:ParallelGCThreads=4 # 并行GC线程数(STW阶段)
-XX:ConcGCThreads=2 # 并发GC线程数(并发阶段)
ZGC 收集器 Z Garbage Collector #
新一代低延迟垃圾收集器,JDK 15 正式可用 ⭐⭐⭐⭐⭐
核心设计目标 #
- 停顿时间不超过 10ms(无论堆多大)
- 支持最大 16TB 堆内存
- 吞吐损失不超过 15%(相对于G1)
- 读屏障 + 染色指针 + 并发整理
关键技术 #
1. 染色指针 Colored Pointers #
利用64位指针的未使用位(高18位)存储对象标记信息:
优点:
- 标记信息直接存在指针里,不需要额外空间存储
- 读取屏障实现并发标记,不需要STW
2. 多重映射 Multi-mapping #
同一个对象物理页,映射到三个不同虚拟地址:
- Address0 / Address1 / Address2
- 用哪个地址表示当前标记颜色
- 不需要扫描对象头就能知道标记
3. 读屏障 Load Barrier #
ZGC 使用读屏障实现染色指针的转发:
- 用户读取对象引用时,经过读屏障处理
- 根据染色位做相应转发
- 整个标记过程完全并发,不需要STW
ZGC 回收过程 #
所有STW阶段都非常短(通常几毫秒),与堆大小无关
ZGC 特点 #
| 特性 | 说明 |
|---|---|
| 整堆并发 | 大部分工作并发,STW极短 |
| 着色指针 | 标记信息存在指针,压缩空间 |
| 读屏障 | 实现并发标记,不需要移动对象头 |
| 压缩整理 | 每次GC都压缩,无内存碎片 |
| 分区 | 基于页(page)分配,灵活 |
| 停顿 | < 10ms,和堆大小无关 |
| 最大堆 | 支持 16TB |
分代 ZGC(JDK 21+) #
ZGC 最初是不分代的,JDK 21 引入分代ZGC:
- 年轻代 + 老年代分开收集
- 年轻代回收更快,减少整体停顿
- 进一步降低GC停顿时间和吞吐损失
配置示例 #
# JDK 15+ 正式可用,不需要 UnlockExperimental
-XX:+UseZGC
# JDK 11-14 需要解锁实验性
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
# 分代ZGC(JDK 21+)默认开启
-XX:+ZGenerational
ZGC vs G1 对比 #
| 对比项 | G1 | ZGC |
|---|---|---|
| 最大堆 | 数十GB | 16TB |
| 平均停顿 | 100-200ms | 1-10ms |
| 停顿与堆大小 | 堆越大停顿越长 | 无关 |
| 内存碎片 | 混合回收整理,较少碎片 | 每次压缩,无碎片 |
| 吞吐 | 更高 | 略低(损失 ~10-15%) |
| 适用场景 | 一般服务端应用 | 低延迟要求,大堆,云原生 |
Shenandoah 收集器 #
Red Hat 开发的另一款低延迟收集器,特点:
- 和ZGC类似,目标是低停顿
- 使用读屏障 + 布鲁克汉算法(Brickhan)并发压缩
- OpenJDK 12+ 集成,JDK 17+ 成熟
- 停顿时间也在十毫秒级别
GC 触发条件 #
| GC类型 | 触发条件 | 范围 |
|---|---|---|
| Minor GC | Eden区满 | 年轻代 |
| Full GC | 老年代空间不足 | 整个堆 |
| Major GC | 大对象直接进入老年代 | 老年代 |
GC 触发条件(扩展) #
Minor GC (Young GC) 触发场景 #
| 触发场景 | 说明 | 频率 |
|---|---|---|
| Eden区满 | 新对象分配时Eden空间不足,这是最常见的触发原因 | 频繁 |
| 对象年龄晋升 | Survivor区存活对象年龄达到MaxTenuringThreshold,晋升到老年代时可能伴随Minor GC | 中等 |
| Survivor区空间不足 | Survivor区无法容纳Eden区存活对象,直接晋升老年代 | 较少 |
Full GC (Major GC) 触发场景 #
| 触发场景 | 说明 | 风险等级 |
|---|---|---|
| 老年代空间不足 | 大对象直接分配、对象晋升、年龄积累导致老年代满 | 🔴 高 |
| 元空间(Metaspace)不足 | 加载类太多、动态生成类(如cglib、反射)导致元空间满 | 🟡 中 |
| 显示调用System.gc() | 代码中调用System.gc()或Runtime.getRuntime().gc() | 🟡 中 |
| CMS GC失败 | CMS并发模式失败(concurrent mode failure),退化到Full GC | 🔴 高 |
| 分配担保失败 | Minor GC前检查老年代剩余空间是否足够,不足时触发Full GC | 🟡 中 |
| jmap -histo:live / jmap -dump:live | 手动触发堆dump前的Full GC | 🟢 低 |
| 长期存活对象积累 | 缓存、连接池等长期存活对象导致老年代慢慢填满 | 🟡 中 |
G1 GC 特殊触发场景 #
| 触发场景 | 说明 |
|---|---|
| Young GC | Eden区满,和其他收集器一样 |
| Mixed GC | IHOP(InitiatingHeapOccupancyPercent)阈值触发,默认45% |
| Remark | 并发标记后的重新标记,STW |
| Full GC | G1退化到Full GC(应尽量避免) |
避免Full GC策略 #
// 1. 大对象直接进老年代
-XX:PretenureSizeThreshold=3M
// 2. 增大年轻代
-XX:NewRatio=1
// 3. 调整晋升年龄
-XX:MaxTenuringThreshold=15
// 4. 禁用显式GC
-XX:+DisableExplicitGC
// 5. 监控元空间
-XX:MetaspaceSize=256m
类加载 #
类加载过程 #
| 阶段 | 说明 |
|---|---|
| 加载 | 读取class字节码 |
| 验证 | 字节码安全性 |
| 准备 | 为静态变量分配内存(零值) |
| 解析 | 符号引用转直接引用 |
| 初始化 | 执行静态代码块、赋静态变量初值 |
类加载器 #
为什么要双亲委派?
- 安全性: 避免自定义类替换核心类
- 唯一性: 确保类只被加载一次
类加载时机 #
| 触发动作 | 说明 |
|---|---|
new |
创建实例 |
反射 |
调用Class.forName() |
子类加载 |
初始化父类 |
main |
启动类 |
性能调优 #
调优目标 #
调优步骤 #
1. 监控工具 #
# 命令行工具
jps # 进程查看
jstat -gcutil # GC统计
jmap -heap # 堆内存
jstack # 线程堆栈
jmap -dump # 堆转储
# GUI工具
jconsole # 监控管理
jvisualvm # 可视化
Arthas # 诊断工具
jcmd # JVM诊断
2. GC 日志配置 #
# Java 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-Xloggc:/path/gc.log
# Java 11+
-Xlog:gc*:file=/path/gc.log:time,uptime,level,tags
3. 堆转储分析 #
# 生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
# 分析工具
- MAT (Memory Analyzer Tool)
- VisualVM
- JDK Mission Control
不同场景调优方案 #
Web 服务(低延迟) #
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
大数据计算(高吞吐) #
-Xms16g -Xmx16g
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:GCTimeRatio=99 # GC时间不超过1%
微服务(小内存) #
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
OOM 问题排查 #
常见泄漏原因:
- 静态集合未清理
- ThreadLocal 未 remove
- 数据库连接未关闭
- 监听器未注销
- 缓存未限制大小
JVM 常量池 #
| 常量池 | 位置 | 内容 |
|---|---|---|
| Class文件常量池 | .class文件 | 编译期字面量、符号引用 |
| 运行时常量池 | 方法区(元空间) | 动态加载的常量 |
| 字符串常量池 | 堆内存(JDK7+) | 字符串唯一引用 |
String s1 = "java"; // 常量池
String s2 = new String("java"); // 堆对象
String s3 = s2.intern(); // 放入常量池
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // true
JDK版本演进 #
JDK 8 以后各版本主要新增特性总结
JDK 8 (2014 - LTS) ⭐⭐⭐⭐⭐⭐ #
最经典的长期支持版本,企业界使用最广泛
主要新增:
| 特性 | 说明 |
|---|---|
| Lambda 表达式 | 函数式编程,简化匿名内部类 |
| Stream API | 集合流式操作,支持函数式编程 |
| Functional Interface | 函数式接口注解 @FunctionalInterface |
| 默认方法 | 接口可以有默认方法实现 |
| 重复注解 | 允许同一个注解多次使用 |
| 方法引用 | :: 语法,进一步简化Lambda |
| Optional | 优雅处理空指针 |
| CompletableFuture | 异步编程支持 |
DateTime API (java.time) |
全新的日期时间API,取代Date/Calendar |
| Nashorn JavaScript 引擎 | 内置JS引擎 |
| ConcurrentHashMap 优化 | 分段锁改进,性能提升 |
| PermGen → Metaspace | 永久代移除,元空间直接使用本地内存 |
JDK 9 (2017) #
模块化开发,奠定基础
| 特性 | 说明 |
|---|---|
| 模块系统 (JPMS) | module-info.java,模块化开发 |
| G1 成为默认垃圾收集器 | 替换Parallel GC |
| AOT 编译 实验性 | 提前编译 |
| JShell | 交互式命令行工具(REPL) |
| 进程API改进 | ProcessHandle 获取进程信息 |
| 变量句柄 VarHandle | 替代Unsafe的部分功能 |
| 平台日志API | System.Logger |
| HTTP 2 客户端 | jdk.incubator.httpclient |
| 字符串拼接优化 | invokedynamic 优化 |
JDK 10 (2018) #
局部变量类型推断
| 特性 | 说明 |
|---|---|
var 关键字 |
局部变量类型推断 var list = new ArrayList<String>(); |
| 并行Full GC改进 | G1并行Full GC,降低停顿 |
| 应用类数据共享 | 提升启动速度 |
| 线程局部管控 | 内存优化 |
JDK 11 (2018 - LTS) ⭐⭐⭐⭐⭐ #
第二个长期支持版本,广泛生产使用
| 特性 | 说明 |
|---|---|
| ZGC | 新一代低延迟垃圾收集器(实验性) |
| Epsilon GC | 无操作GC,测试性能基准 |
| HTTP Client 标准化 | java.net.http 正式支持HTTP/2 |
| 增加字符串方法 | String.isBlank() lines() repeat() strip() |
| 局部变量语法增强 | var 可以用于lambda参数 |
| 移除 | Java EE模块,CORBA,JavaFX,Nashorn不全移除 |
| 低开销堆分析 | JVM自带HeapDump分析 |
| Lambda 匿名内部类嵌套访问优化 | 反射访问权限开放 |
JDK 12 (2019) #
| 特性 | 说明 |
|---|---|
| Shenandoah GC 红船GC(实验性) | OpenJDK新增低延迟GC |
| G1 可默认并发类卸载 | 减少内存占用 |
| 微基准测试框架 JMH 集成到JDK | |
| JVM 常量API | java.lang.constant |
JDK 13 (2019) #
| 特性 | 说明 |
|---|---|
| ZGC 并发类卸载 | 进一步降低内存占用 |
| Socket 连接实现重构 | 替换旧的NIO实现 |
文本块预览 """ |
多行文本块,不用拼接转义 |
| Switch 表达式二次预览 |
JDK 14 (2020) #
| 特性 | 说明 |
|---|---|
| ZGC 移除实验性标记 | 正式可用 |
| NullPointerException 增强 | 准确提示哪个对象是null |
| 记录类 Record 预览 | 不可变数据类 |
| Switch 表达式转正 | 正式特性 |
| 模式匹配 instanceof 预览 | if (obj instanceof String s) 直接绑定变量 |
| 移除 CMS 垃圾收集器 | CMS正式退役 |
JDK 15 (2020) #
| 特性 | 说明 |
|---|---|
| Sealed 类预览 | 密封类,限制继承 |
| 文本块转正 | 正式特性 |
| ZGC 支持macOS | 之前只支持Linux |
| Edwards-Curve 数字签名 | ed25519 |
| 移除 Nashorn JavaScript 引擎 | 完全移除 |
JDK 16 (2021) #
| 特性 | 说明 |
|---|---|
| Record 转正 | 正式特性,轻便数据载体 |
| 模式匹配 instanceof 转正 | 正式特性 |
| Sealed 类转正 | 正式特性 |
| 值类型 Flatten 存储 孵化器 | |
端口号范围默认 0 → 1024 |
JDK 17 (2021 - LTS) ⭐⭐⭐⭐⭐ #
第三个长期支持版本,新一代生产主流
| 特性 | 说明 |
|---|---|
| ZGC 并发栈处理 | 低延迟进一步改进 |
| Sealed 类正式特性 | |
| macOS/AArch64 支持 | 支持苹果M芯片 |
| 移除 AWT 依赖,旧的Applet,RMI activation,Security Manager | |
| Vector API 孵化器 | 向量计算,支持SIMD |
| 字符串拼接坚持用invokedynamic |
JDK 18 (2022) #
| 特性 | 说明 |
|---|---|
简单web服务器 jwebserver |
内置小型静态文件服务器 |
| UTF-8 默认字符集 | 整个JDK默认UTF-8 |
| CodeQL 集成到OpenJDK | 代码安全扫描 |
| Vector API 第四次孵化器 |
JDK 19 (2022) #
| 特性 | 说明 |
|---|---|
| 虚拟线程 (Project Loom) 转正 | ⭐ 轻量级用户态线程,海量并发 |
| 模式匹配 switch 预览 |
JDK 20 (2023) #
| 特性 | 说明 |
|---|---|
| 虚拟线程稳定 | |
| 值类型 再次孵化器 |
JDK 21 (2023 - LTS) ⭐⭐⭐⭐ #
最新长期支持版本,新一代生产首选
| 特性 | 说明 |
|---|---|
| 虚拟线程正式发布 | Thread.startVirtualThread() 原生支持百万级并发 |
| 分代ZGC | ZGC正式支持分代收集,进一步提升性能 |
| 模式匹配 switch 转正 | 模式匹配完整支持 |
| 记录模式转正 | 配合record模式匹配解构 |
| 密封类完成进化 | |
| 未命名模式和变量 | var _ 忽略不需要的组件 |
| Scoped Values 孵化器 | 共享数据而不用线程局部 |
| 移除了Windows 32位端口 |
JDK 版本选择建议 #
| 使用场景 | 推荐版本 | 理由 |
|---|---|---|
| 企业遗留项目 | JDK 8 | 生态成熟,稳定性高 |
| 新项目兼容要求高 | JDK 11 | 第二个LTS,足够稳定 |
| 云原生微服务 | JDK 17 | 新特性多,ZGC低延迟,虚拟线程预热 |
| 前沿生产 | JDK 21 | 虚拟线程完整支持,分代ZGC,最新稳定 |
| 学习测试 | JDK 21 | 最新特性,未来趋势 |
LTS 版本时间表 #
| 版本 | 发布日期 | 终止公开更新 |
|---|---|---|
| JDK 8 | 2014 | 2030 (Oracle) / 长期更新(OpenJDK) |
| JDK 11 | 2018 | 2026 |
| JDK 17 | 2021 | 2029 |
| JDK 21 | 2023 | 2031 |
GC 发展主线 #
🎯 面试题汇总 #
基础 #
- JVM内存结构?
答:
JVM内存结构分为以下五个区域:
| 区域 | 作用 | 线程私有 | 是否GC | 特点 |
|---|---|---|---|---|
| 堆(Heap) | 存放对象实例和数组,GC主要区域 | ❌ 共享 | ✅ 是 | 最大一块,年轻代+老年代 |
| 方法区(Method Area) | 存储类信息、常量、静态变量 | ❌ 共享 | ✅ 是 | JDK8前叫永久代,8后叫元空间(Metaspace) |
| 虚拟机栈(VM Stack) | 方法调用栈帧,局部变量表、操作数栈 | ✅ 私有 | ❌ 否 | 每个线程一个,栈帧入栈出栈 |
| 本地方法栈(Native Stack) | Native方法调用栈 | ✅ 私有 | ❌ 否 | 类似虚拟机栈,服务于native方法 |
| 程序计数器(PC Register) | 当前线程执行的字节码行号指示器 | ✅ 私有 | ❌ 否 | 唯一个不会OOM的区域 |
注意: JDK 8 中永久代被移除,改用元空间(Metaspace),使用本地内存,不在JVM堆中。
- 堆内存分代模型?
答:
堆内存分为年轻代和老年代:
比例: Eden : S0 : S1 = 8 : 1 : 1 | 配置:
-XX:SurvivorRatio=8
年轻代(Young Generation):
- Eden区:新对象分配在这里,满了触发Minor GC
- Survivor区:S0和S1,复制算法使用,存活对象在S0和S1之间来回移动
- 比例:默认Eden:S0:S1 = 8:1:1
老年代(Old Generation):
- 长期存活的对象进入老年代
- 对象在年轻代存活一定年龄(默认15次GC)后晋升
- 大对象(超过
-XX:PretenureSizeThreshold)直接分配到老年代 - 老年代空间不足触发Full GC
为什么分代?
- 不同代对象生命周期不同,采用不同回收算法
- 年轻代对象朝生夕死,用复制算法
- 老年代对象存活时间长,用标记-整理算法
- GC Roots类型?
答:
GC Roots是垃圾回收的起点,从这些对象开始向下搜索,不可达的对象就是垃圾。
GC Roots包括:
| 类型 | 说明 |
|---|---|
| 虚拟机栈中引用的对象 | 栈帧中局部变量表引用的对象 |
| 方法区中静态变量引用的对象 | static字段引用的对象 |
| 方法区中常量引用的对象 | final常量引用的对象 |
| 本地方法栈中JNI引用的对象 | Native方法引用的对象 |
| 活跃线程 | 当前正在运行的线程 |
| 同步锁(synchronized) | 锁对象 |
| JVM内部引用 | Class对象、异常对象、系统类加载器等 |
注意: GC Roots必须是堆外指向堆内的引用,堆内对象互相引用不算GC Roots。
- 四种引用类型区别?
答:
Java有四种引用类型,强度从强到弱:
| 引用类型 | 强度 | GC回收时机 | 用途 | 实现类 |
|---|---|---|---|---|
| 强引用 | ★★★★★ | 从不回收 | 普通对象引用 | 默认new出来的 |
| 软引用 | ★★★★ | 内存不足时回收 | 缓存 | SoftReference |
| 弱引用 | ★★★ | 下次GC必定回收 | WeakHashMap | WeakReference |
| 虚引用 | ★ | GC时通知,不影响回收 | 对象回收跟踪 | PhantomReference |
代码示例:
// 强引用 - 最常用,OOM也不会回收
Object strongRef = new Object();
// 软引用 - 内存不足时才回收
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 弱引用 - 下次GC必定回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 虚引用 - 仅用于跟踪回收,必须配合ReferenceQueue
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
常见应用:
- 强引用:普通对象
- 软引用:图片缓存、浏览器缓存(内存不够才释放)
- 弱引用:
WeakHashMap、ThreadLocal - 虚引用:堆外内存回收跟踪、Debug
GC #
- Minor GC vs Full GC?
答:
| 对比项 | Minor GC(Young GC) | Full GC(Major GC) |
|---|---|---|
| 回收区域 | 仅年轻代 | 整个堆(年轻代+老年代)+ 元空间 |
| 触发条件 | Eden区满 | 老年代空间不足 / 元空间不足 / System.gc() |
| 执行频率 | 频繁 | 较少 |
| 停顿时间 | 较短 | 较长 |
| 速度 | 快 | 慢 |
详细说明:
Minor GC:
- Eden区满时触发
- 使用复制算法,存活对象复制到Survivor区
- 存活年龄足够的对象晋升到老年代
- 通常很快,因为年轻代存活对象少
Full GC:
- 老年代空间不足时触发
- 通常伴随一次Young GC
- 收集整个堆,停顿时间长
- 应尽量避免
避免Full GC策略:
# 1. 增大年轻代,减少对象晋升
-XX:NewRatio=2
# 2. 增大老年代空间
-Xmx4g
# 3. 调整晋升年龄阈值
-XX:MaxTenuringThreshold=15
# 4. 大对象直接进老年代,避免在年轻代频繁复制
-XX:PretenureSizeThreshold=3m
# 5. 禁用显式GC调用
-XX:+DisableExplicitGC
- 有哪些GC算法?标记清除/标记整理/复制算法区别?
答:
三种经典GC算法对比:
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 先标记垃圾,再统一清除 | 简单,不需要移动对象 | 内存碎片,分配大对象困难 | 老年代,对象存活率高 |
| 标记-整理 | 标记后,存活对象向一端移动,清除边界外 | 无内存碎片 | 需要移动对象,效率低 | 老年代,CMS remark后 |
| 复制 | 分为两块,只使用一块,回收时存活对象复制到另一块 | 无碎片,简单高效 | 需要额外空间,存活对象多则复制成本高 | 年轻代,朝生夕死 |
图解:
为什么年轻代用复制算法?
- 年轻代98%对象朝生夕死,存活少,复制成本低
- 没有内存碎片
为什么老年代不用复制算法?
- 老年代对象存活率高,复制成本高
- 没有额外空间浪费
- Serial/Parallel/CMS/G1/ZGC 各自特点?适用场景?
答:
垃圾收集器对比总结:
| 收集器 | 年轻代算法 | 老年代算法 | 目标 | 适用场景 | JDK版本 |
|---|---|---|---|---|---|
| Serial | 复制 | 标记-整理 | 单线程 | 客户端应用/单CPU | 所有版本保留 |
| Parallel | 复制(多线程) | 标记-整理(多线程) | 高吞吐量 | 后台计算/批处理 | JDK8默认 |
| CMS | ParNew(多线程) | 标记-清除(并发) | 低停顿 | Web服务/低延迟 | JDK9移除 |
| G1 | 复制(多线程) | 标记-整理 + 混合回收 | 可预测停顿 | 服务端/大堆 | JDK9默认 |
| ZGC | 并发标记 + 并发整理 | 整堆并发 | 极低停顿 | 大内存/低延迟 | JDK15正式 |
详细特点:
Serial(串行收集器):
- 单线程GC,STW(Stop-The-World)
- 简单高效,没有线程交互开销
- 客户端模式(Client VM)默认收集器
Parallel(并行收集器):
- 多线程并行GC,仍STW
- 目标是高吞吐量(GC时间/总时间最小)
- 适合后台任务、批处理、大数据计算
CMS(Concurrent Mark Sweep):
- 目标:最短停顿时间
- 并发收集,大部分时间和用户线程一起运行
- 缺点:内存碎片、浮动垃圾
- JDK 9 被移除,被G1替代
G1(Garbage-First):
- Region分区,不固定年轻代老年代
- 优先回收垃圾多的Region
- 可预测停顿时间模型
- 服务端大堆应用推荐
ZGC(Z Garbage Collector):
- 停顿时间<10ms,与堆大小无关
- 支持最大16TB堆
- 染色指针技术,读屏障
- 低延迟优先场景
选择建议:
- 小堆/单CPU → Serial
- 追求吞吐量 → Parallel
- 追求低延迟 → G1
- 大堆/极低延迟 → ZGC
- CMS收集器四阶段?为什么CMS被移除?
答:
CMS四个阶段:
| 阶段 | 是否STW | 说明 | 耗时 |
|---|---|---|---|
| 初始标记 | ✅ STW | 仅标记GC Roots直接关联的对象 | 很快 |
| 并发标记 | ❌ 并发 | 和用户线程一起,遍历整个对象图 | 慢,但不暂停 |
| 重新标记 | ✅ STW | 修正并发标记期间用户程序导致的标记变动 | 中等 |
| 并发清除 | ❌ 并发 | 清除垃圾对象,和用户线程一起 | 慢,但不暂停 |
CMS为什么被移除?
-
内存碎片问题
- 使用标记-清除算法,会产生内存碎片
- 碎片过多时,分配大对象找不到连续空间
- 可能提前触发Full GC
-
浮动垃圾
- 并发清除阶段,用户线程还在运行,产生新垃圾
- 这些新垃圾只能等下次GC回收
- 需要预留空间给用户线程
-
对CPU资源敏感
- 并发阶段占用CPU线程
- 核心少的服务器影响吞吐量
-
G1更优秀
- G1解决了CMS的内存碎片问题
- G1有可预测停顿模型
- G1性能更好
JDK版本:
- JDK 9:CMS被标记为Deprecate
- JDK 14:CMS被完全移除
- G1收集器原理?Region分区优势?
答:
G1核心设计思想:Region分区
G1不再固定年轻代和老年代的物理边界,而是将堆分为多个大小相等的Region:
Region特点:
- 每个Region大小:1MB ~ 32MB,根据堆大小自动选择
- 可以是Eden、Survivor、Old、Humongous中的任一种
- 大对象(超过Region的一半)直接放在连续的Humongous Region
G1回收过程:
Region分区的优势:
-
不需要固定分代大小
- 可根据应用动态调整年轻代和老年代大小
- 更灵活
-
局部收集
- 不需要每次都收集整个老年代
- 只选择垃圾多的Region,收益高
-
内存整理
- 复制算法,无内存碎片
- 解决了CMS的碎片问题
-
可预测停顿
- 根据历史数据,预测回收多少Region能满足停顿目标
- 用户可设置
-XX:MaxGCPauseMillis=200
- G1怎么实现可预测停顿?
答:
可预测停顿模型是G1最大特色:
用户可以通过参数设置期望的最大停顿时间:
-XX:MaxGCPauseMillis=200 # 默认200ms
G1如何实现:
-
收集停顿历史数据
- G1持续跟踪每个Region的回收耗时
- 建立数据库:回收这个Region需要多少时间,能回收多少垃圾
-
计算收益性价比
- 每次GC前,根据历史数据预测
- 选择"回收价值/耗时"最高的Region
- 优先回收垃圾多、回收快的Region
-
控制收集范围
- 不一次性回收所有Region
- 根据停顿目标,选择合适数量的Region
- 年轻代Region + 部分收益高的老年代Region = Mixed GC
-
动态调整
- 如果某次GC超过了目标,下次减少收集Region数
- 如果没达到,可以多收集一些
Remembered Set(记忆集)加速:
每个Region都有自己的Remembered Set,记录其他Region指向本Region的引用:
- 不需要扫描全堆,只扫描Remembered Set
- 加速Young GC和Mixed GC
注意: 可预测是"尽量满足",不是"绝对保证",如果存活对象太多还是可能超过目标。
- ZGC染色指针原理?为什么停顿这么短?
答:
ZGC的三个核心技术:
- 染色指针(Colored Pointers)
- 读屏障(Load Barrier)
- 多重映射(Multi-Mapping)
染色指针原理:
利用64位指针的高位存储对象标记信息(注意:64位指针实际只用到48位寻址):
4个标记位含义:
marked0/marked1:三色标记使用remapped:对象是否被移动(重映射)finalizable:是否可 finalize
为什么这样设计?
- 标记信息直接在指针上,不需要在对象头存
- 读取指针时通过读屏障处理,不需要STW
ZGC停顿为什么这么短?
| ZGC阶段 | 是否STW | 说明 |
|---|---|---|
| 初始标记 | ✅ 短 | 仅标记GC Roots |
| 并发标记 | ❌ | 和用户线程并发 |
| 再标记 | ✅ 极短 | 修正并发标记 |
| 并发准备 | ❌ | 准备转移 |
| 初始转移 | ✅ 极短 | Roots重映射 |
| 并发转移 | ❌ | 并发复制对象 |
所有STW阶段都和堆大小无关!
- 只和GC Roots数量有关,时间很短(毫秒级)
- 不论堆是8GB还是1TB,停顿都差不多
ZGC怎么做到并发转移?
- 读屏障(Load Barrier):读取对象指针时,检查是否已被移动
- 如果对象被移动了,指针指向新地址(指针"自愈")
- 染色指针让对象移动也不需要修正所有引用
- ZGC和G1对比,各自适用场景?
答:
| 对比项 | G1 | ZGC |
|---|---|---|
| 最大堆支持 | 几十GB | 16TB |
| 平均停顿 | 100-500ms | 1-10ms |
| 停顿与堆大小关系 | 堆越大停顿越长 | 无关 |
| 内存碎片 | 混合回收整理,较少 | 每次都压缩,无碎片 |
| 吞吐量 | 高 | 略低(损失~10-15%) |
| 内存占用 | 正常 | 略高 |
| 分代收集 | ✅ 分代 | ✅ JDK21+分代 |
| JDK版本要求 | JDK9+默认 | JDK15+正式 |
| 适用场景 | 一般服务端应用 | 低延迟要求、大堆、云原生 |
G1适用场景:
- 服务端应用,4-32GB堆
- 需要可预测停顿
- 追求吞吐量和延迟的平衡
- 大多数业务场景首选
ZGC适用场景:
- 极低延迟要求(毫秒级)
- 大堆(32GB+)
- 云原生、微服务
- 对停顿敏感的交易系统
配置示例:
# G1配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
# ZGC配置
-XX:+UseZGC
-XX:ConcGCThreads=2 # 并发GC线程数
选择建议:
- JDK 8 → Parallel(吞吐量)或CMS(低延迟)
- JDK 9-14 → G1(默认)
- JDK 15+ → G1(通用)或ZGC(低延迟)
- JDK 21+ → ZGC + 分代(性能更好)
- 什么是三色标记法?
答:
三色标记法是并发标记的基础算法:
将对象分为三种颜色:
| 颜色 | 含义 | 说明 |
|---|---|---|
| 白色 | 未标记 | 垃圾对象,最后会被回收 |
| 灰色 | 正在标记 | 自身已标记,但成员变量还没处理完 |
| 黑色 | 已标记 | 自身和所有成员变量都处理完,安全存活 |
标记过程:
步骤:
- GC Roots初始标记为黑色
- 从GC Roots开始,引用的对象标记为灰色
- 处理灰色对象,其引用的对象也变灰,自己变黑
- 直到没有灰色对象
- 剩下的白色对象就是不可达垃圾
三色标记的漏标问题:
并发标记时,用户线程可能修改引用关系,导致应该存活的对象被标记为白色(漏标):
漏标两个必要条件:
- 黑色对象指向白色对象(新增引用)
- 灰色对象不再指向白色对象(删除引用)
解决方案:
- Incremental Update(增量更新):CMS使用,黑色对象新增引用时重新标记
- SATB(Snapshot At The Beginning):G1使用,保存开始时的快照
- 读屏障:ZGC使用,读取时检查并修正
- 如何优化GC时间?
答:
GC优化原则和步骤:
第一步:监控分析
# 开启GC日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags # JDK11+
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log # JDK8
# 监控工具
jstat -gcutil <pid> 1000 # 每1秒打印GC统计
jmap -heap <pid> # 查看堆内存
jvisualvm / JProfiler # 可视化分析
第二步:参数调优
年轻代调优:
# 年轻代大小建议:堆的30-40%
-Xmn2g
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 晋升年龄
-XX:MaxTenuringThreshold=15
# 大对象直接进老年代
-XX:PretenureSizeThreshold=3m
收集器选择:
# 吞吐量优先
-XX:+UseParallelGC
# 低延迟优先(JDK9+)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
# 极低延迟(JDK15+)
-XX:+UseZGC
第三步:代码层面优化
| 问题 | 优化 |
|---|---|
| 频繁创建大对象 | 对象池/复用 |
| 内存泄漏 | 修复泄漏点 |
| 长生命周期对象 | 检查是否应该缓存 |
| 集合扩容 | 合理设置初始容量 |
第四步:架构层面优化
- 微服务拆分,减少单实例内存压力
- 读写分离,降低GC压力
- 缓存设计,减少对象创建
优化目标:
- 减少Full GC频率(最好几天一次)
- 控制每次GC停顿时间
- 吞吐量和延迟平衡
类加载 #
- 类加载过程?
答:
类加载全过程:
详细步骤:
| 阶段 | 说明 | 动作 |
|---|---|---|
| 加载 | 获取字节码 | 通过类名获取二进制流,在内存生成Class对象 |
| 验证 | 确保字节码安全 | 文件格式、元数据、字节码、符号引用验证 |
| 准备 | 分配内存赋零值 | 静态变量分配内存,设为初始零值(0/false/null) |
| 解析 | 符号引用转直接引用 | 把常量池的符号引用替换为内存地址的直接引用 |
| 初始化 | 执行<clinit> |
静态代码块执行,静态变量赋初始值 |
注意:
- 准备阶段只赋零值:
static int a = 1;→ 准备阶段a=0,初始化阶段才=1 - 初始化时,父类优先于子类初始化
- 接口初始化不会触发父接口初始化
类初始化触发时机(有且只有):
new对象- 访问静态字段(非final)或静态方法
Class.forName()反射- 子类初始化时,先初始化父类
- 主类启动(有main方法)
- MethodHandle/JDK动态语言支持
- 双亲委派机制及破坏?
答:
双亲委派模型:
工作原理:
- 类加载器收到请求,先不自己加载
- 委派给父类加载器(向上委派)
- 父类加载不了,才自己加载(向下尝试)
- 父优先,保证基础类只加载一次
为什么要双亲委派?
- 安全:防止自定义类替换核心类(如
java.lang.String) - 唯一:保证同一个类只被加载一次
- 有序:类层次清晰
双亲委派的破坏:
破坏1:JDK 1.2之前,自定义类加载器直接重写loadClass()
- 后来增加findClass()让用户重写,保持双亲委派
破坏2:线程上下文类加载器(Thread Context ClassLoader)
- 解决SPI问题:JDBC/JNDI等,父类加载器需要加载子类的代码
- 通过
Thread.setContextClassLoader()设置上下文类加载器
破坏3:OSGi等模块化框架
- 动态热部署,类加载器网状结构
- 不严格按父子关系
- 类加载时机?
答:
主动引用(触发初始化):
| 场景 | 示例 |
|---|---|
| new实例化对象 | new User() |
| 访问静态字段(非final) | User.count |
| 调用静态方法 | User.doSomething() |
| 反射调用 | Class.forName("com.User") |
| 初始化子类 | 子类初始化前先初始化父类 |
| 主类启动 | public static void main(String[] args) |
被动引用(不触发初始化):
| 场景 | 说明 | 示例 |
|---|---|---|
| 引用父类静态字段 | 只会初始化父类 | System.out.println(Parent.value) |
| 数组定义 | 不会初始化元素类型 | User[] users = new User[10]; |
| 引用编译期常量 | 常量在编译阶段存入调用类常量池 | System.out.println(Const.PI) |
示例代码:
class Parent {
static { System.out.println("Parent init"); }
static int value = 100;
}
class Child extends Parent {
static { System.out.println("Child init"); }
}
class Const {
static final int PI = 314; // 编译期常量
static { System.out.println("Const init"); }
}
// 测试1:只引用父类静态字段 → 只初始化Parent
System.out.println(Child.value);
// 输出:Parent init
// 100
// 测试2:数组 → 不初始化
Child[] arr = new Child[10];
// 输出:无
// 测试3:编译期常量 → 不初始化
System.out.println(Const.PI);
// 输出:314(Const没有初始化!)
调优 #
- JVM常用参数?
答:
堆内存设置:
# 初始堆和最大堆建议设成一样,避免动态扩容
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小
-Xmn2g # 年轻代大小
-XX:NewRatio=2 # 老年代:年轻代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
元空间(方法区):
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小
栈大小:
-Xss512k # 每个线程栈大小
垃圾收集器选择:
# G1收集器(JDK9+默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
# ZGC收集器(JDK15+)
-XX:+UseZGC
# Parallel收集器(JDK8默认)
-XX:+UseParallelGC
-XX:+UseParallelOldGC
GC日志:
# JDK8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
# JDK11+
-Xlog:gc*:file=gc.log:time,uptime,level,tags
OOMdump:
# OOM时自动dump堆
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
常用调优组合示例:
# Web服务(低延迟,G1)
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
# 大数据计算(高吞吐,Parallel)
-Xms16g -Xmx16g
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
# 微服务(小内存,G1)
-Xms1g -Xmx1g
-XX:+UseG1GC
- 如何排查OOM?
答:
OOM排查流程:
第一步:获取堆转储
# 参数配置自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
# 手动dump(即使没OOM)
jmap -dump:format=b,file=heap.hprof <pid>
第二步:分析工具
| 工具 | 特点 |
|---|---|
| MAT(Memory Analyzer) | 功能最强,Leak Suspects报告 |
| JVisualVM | JDK自带,可视化 |
| JProfiler/YourKit | 商业工具,功能强 |
| jhat | JDK自带,命令行,已过时 |
MAT关键分析视图:
- Histogram:查看对象数量和大小
- Dominator Tree:查看大对象支配关系
- Leak Suspects:自动分析泄漏怀疑点
- Path to GC Roots:查看对象引用链
常见OOM原因:
| 原因 | 现象 | 解决 |
|---|---|---|
| 内存泄漏 | 对象不用了但还被引用 | 修复泄漏点(静态集合/ThreadLocal) |
| 大对象过多 | 大数组/大集合占用 | 优化代码,减少大对象 |
| 堆内存不足 | 确实需要更大堆 | 调大-Xmx |
| 元空间溢出 | 类太多/动态生成类 | 调大-XX:MaxMetaspaceSize |
内存泄漏常见场景:
- 静态集合未清理
- ThreadLocal未remove
- 数据库连接未关闭
- 监听器未注销
- 缓存未限制大小
- 有哪些监控工具?
答:
命令行工具(JDK自带):
| 工具 | 作用 | 示例 |
|---|---|---|
| jps | 查看Java进程 | jps -l |
| jstat | GC统计、内存 | jstat -gcutil <pid> 1000 |
| jmap | 堆内存分析、dump | jmap -heap <pid> |
| jstack | 线程堆栈 | jstack <pid> |
| jcmd | 多功能诊断 | jcmd <pid> GC.heap_dump dump.hprof |
| jinfo | JVM参数查看/修改 | jinfo -flags <pid> |
可视化工具:
| 工具 | 特点 |
|---|---|
| JVisualVM | JDK自带,功能全 |
| JProfiler | 商业,功能强 |
| Arthas | Alibaba开源,在线诊断神器 |
| Prometheus + Grafana | 监控告警 |
| SkyWalking | APM全链路追踪 |
Arthas常用命令:
# 启动Arthas
as.sh <pid>
# 常用命令
dashboard # 系统概览
thread # 线程分析
watch # 方法执行观测
trace # 方法调用链路
heapdump # 堆dump
GC日志分析工具:
- GCViewer
- GCEasy(在线)
- JDK自带的
jstat
- 深拷贝与浅拷贝?
答:
浅拷贝 vs 深拷贝:
| 对比项 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 对象本身 | ✅ 复制 | ✅ 复制 |
| 引用类型成员 | ❌ 只复制引用,共享同一对象 | ✅ 递归复制对象 |
| 修改影响 | 原对象和拷贝对象互相影响 | 互不影响 |
图解:
实现方式:
// 方式1:实现Cloneable接口 + clone()
class User implements Cloneable {
private String name;
private Address addr;
// 浅拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 默认浅拷贝
}
// 深拷贝
public User deepClone() throws CloneNotSupportedException {
User copy = (User) super.clone();
copy.addr = (Address) addr.clone(); // 引用类型也要拷贝
return copy;
}
}
// 方式2:序列化(最简单的深拷贝)
class DeepCopyUtil {
public static <T> T deepCopy(T obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (T) ois.readObject();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
注意: clone()默认是浅拷贝,要实现深拷贝需要递归复制所有引用类型成员。
JDK版本特性 #
- JDK 8相比JDK 7有哪些重要新特性?
答:
JDK 8是最重要的版本之一,企业广泛使用:
| 特性 | 说明 |
|---|---|
| Lambda表达式 | 函数式编程,简化匿名内部类 |
| Stream API | 集合流式操作,函数式数据处理 |
| FunctionalInterface | 函数式接口注解 |
| 默认方法 | 接口可以有默认方法实现 |
| Optional | 优雅处理空指针 |
| CompletableFuture | 异步编程,组合异步操作 |
| 日期时间API | java.time包,替代Date/Calendar |
| Nashorn JS引擎 | 高性能JS引擎(JDK15移除) |
| ConcurrentHashMap优化 | CAS优化,性能大幅提升 |
| PermGen → Metaspace | 永久代移除,元空间使用本地内存 |
代码示例:
// Lambda表达式
Collections.sort(list, (a, b) -> a.compareTo(b));
// Stream API
List<String> result = list.stream()
.filter(s -> s.length() > 5)
.map(String::toUpperCase)
.collect(Collectors.toList());
// Optional
Optional<User> user = Optional.ofNullable(getUser());
user.map(User::getName).orElse("Unknown");
// 新日期时间API
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
- JDK 11相比JDK 8有哪些重要变化?
答:
JDK 11是第二个LTS长期支持版本:
| 特性 | 说明 |
|---|---|
| ZGC | 新一代低延迟垃圾收集器(实验性) |
| Epsilon GC | 无操作GC,性能测试用 |
| HTTP Client | java.net.http正式支持HTTP/2 |
| 字符串新方法 | isBlank()、lines()、repeat()、strip() |
| var在Lambda | Lambda参数可使用var |
| 飞行记录器 | JFR开源,低开销性能分析 |
| 模块化 | JPMS进一步完善 |
移除的内容:
- Java EE模块(JAX-WS、JAXB等)
- CORBA
- JavaFX(独立出来)
- Nashorn JavaScript引擎(JDK15完全移除)
代码示例:
// HTTP Client(JDK11+)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
// 字符串新方法
" ".isBlank(); // true
"a\nb\nc".lines(); // 流处理
"ab".repeat(3); // "ababab"
" abc ".strip(); // "abc"(比trim更全面)
- JDK 17有哪些重要新特性?
答:
JDK 17是第三个LTS长期支持版本:
| 特性 | 说明 |
|---|---|
| ZGC增强 | 并发栈处理,支持类卸载 |
| Sealed类 | 密封类,限制继承 |
| macOS/AArch64支持 | 苹果M芯片原生支持 |
| 模式匹配switch预览 | switch模式匹配 |
| Record类预览 | 不可变数据载体类 |
| 伪共享优化 | @Contended注解改进 |
Record类示例:
// 自动生成构造器、equals、hashCode、toString
record Point(int x, int y) {}
// 等价于(但更简洁):
class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
// equals/hashCode/toString
}
Sealed类示例:
// 密封类,只允许指定的子类继承
sealed interface Shape permits Circle, Square, Rectangle {}
final class Circle implements Shape {}
final class Square implements Shape {}
final class Rectangle implements Shape {}
- JDK 21的虚拟线程是什么?解决什么问题?
答:
虚拟线程(Virtual Thread)来自Project Loom,JDK21正式转正:
什么是虚拟线程?
- 轻量级用户态线程,由JVM管理,不直接映射操作系统线程
- M:N映射,M个虚拟线程映射到N个平台线程
- 栈放在Java堆上,动态扩容,初始很小(几百字节)
解决的问题:
| 传统平台线程问题 | 虚拟线程解决 |
|---|---|
| 线程昂贵,1MB栈,创建销毁代价大 | 轻量,廉价,百万级也不怕 |
| 几千个线程就到顶 | 支持几十万~几百万并发 |
| IO阻塞时线程也被占用,无法复用 | 阻塞时自动卸载,释放载体线程 |
| 反应式编程复杂(WebFlux) | 保持同步阻塞编程模型,简单 |
代码示例:
// 方式1:直接创建虚拟线程
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Hello virtual thread");
});
// 方式2:虚拟线程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100000; i++) {
executor.submit(() -> handleRequest());
}
executor.close();
// Spring Boot 3.2+ 配置
// application.yaml
spring:
threads:
virtual:
enabled: true // 开启后Web请求都是虚拟线程
对比:
| 对比项 | 平台线程 | 虚拟线程 |
|---|---|---|
| 映射 | 1:1内核线程 | M:N用户态 |
| 栈大小 | 1MB固定 | 几百字节动态 |
| 上下文切换 | 内核态,慢 | 用户态,快 |
| 并发数 | 几千 | 几十万~几百万 |
| 适用场景 | CPU密集 | IO密集,Web服务 |
最佳实践:
- ✅ Web服务、RPC调用、批量处理、消息消费
- ❌ CPU密集计算、超长期线程
- ⚠️ 注意ThreadLocal内存使用,注意synchronized阻塞(JDK21已优化)
- ZGC是什么?相比G1有什么优势?
答:
ZGC(Z Garbage Collector)是新一代低延迟垃圾收集器:
核心特性:
- 停顿时间<10ms,与堆大小无关
- 支持最大16TB堆
- 读屏障 + 染色指针技术
- 整堆并发收集
- 无内存碎片(每次都压缩)
- JDK 15+正式可用,JDK 21+支持分代
相比G1的优势:
| 对比项 | G1 | ZGC |
|---|---|---|
| 停顿时间 | 100-500ms | 1-10ms |
| 停顿与堆大小 | 堆越大停顿越长 | 完全无关 |
| 最大堆 | 几十GB | 16TB |
| 内存碎片 | 混合回收,较少碎片 | 每次压缩,无碎片 |
| 分代收集 | 分代 | JDK21+分代 |
| 吞吐量 | 更高 | 略低(损失~10-15%) |
ZGC的三个核心技术:
- 染色指针:指针高位存标记信息,不需要对象头
- 读屏障:读取指针时处理转发,并发转移
- 多重映射:同一物理页映射不同虚拟地址
适用场景:
- 低延迟要求(交易系统、游戏)
- 大堆(32GB+)
- 云原生微服务
- 对停顿敏感的应用
配置:
-XX:+UseZGC
-XX:ConcGCThreads=2
- 为什么移除CMS垃圾收集器?
答:
CMS(Concurrent Mark Sweep)在JDK9标记为Deprecated,JDK14正式移除:
移除原因:
-
内存碎片问题
- 标记-清除算法,产生内存碎片
- 碎片过多时,大对象分配失败
- 提前触发Full GC
-
浮动垃圾
- 并发清除阶段,用户线程还在运行产生新垃圾
- 新垃圾只能等下次GC回收
- 需要预留空间给用户线程分配对象
-
CPU资源消耗
- 并发阶段占用CPU线程
- 核心少的服务器,吞吐量下降明显
-
G1更优秀
- G1解决了内存碎片问题(复制算法)
- G1有可预测停顿模型
- G1性能更好,功能更全
- G1从JDK9开始就是默认收集器
-
维护成本高
- CMS代码复杂,维护困难
- 不如集中精力优化G1和ZGC
CMS的替代者:
- JDK8 → G1可以使用
- JDK9+ → G1(默认)
- JDK15+ → G1或ZGC
历史回顾:
- JDK 1.4:CMS引入
- JDK 5-6:CMS广泛使用
- JDK 9:G1成为默认,CMS标记Deprecated
- JDK 14:CMS彻底移除
- JDK 15+:ZGC正式可用
- 实际工作中有哪些情况会触发GC?举几个生产环境常见的例子
答:
这是一个非常实战的面试题,面试官想考察你是否真的遇到过GC问题,而不只是背理论。
📊 生产环境常见GC触发场景总结:
🔵 Minor GC (YGC) 常见触发场景 #
| 场景 | 真实案例 | 触发原因 |
|---|---|---|
| Web请求分配对象多 | 电商大促时,每个请求创建大量临时对象,Eden快速填满 | 正常业务流量 |
| 批量处理任务 | 数据导入、报表计算,循环中创建大量临时对象 | 批量操作 |
| 字符串拼接 | 大量"+"拼接或StringBuilder未合理扩容 | 代码问题 |
| 大对象分配失败 | 大数组、大集合在Eden分配不下,直接进老年代 | 对象设计问题 |
💡 真实案例1:电商大促时的YGC
场景:双11大促,每秒上万请求
现象:YGC每2-3秒一次,每次50-100ms
原因:每个请求创建大量临时对象(订单、商品、用户对象)
解决:增大年轻代(-Xmn),优化代码减少临时对象
🔴 Full GC (FGC) 常见触发场景(高危) #
场景1:老年代空间不足 - 最常见
真实案例:缓存系统OOM
现象:Full GC频繁,老年代使用率90%+
原因:Guava Cache没有设置maxSize,缓存对象越来越多
解决:设置cache.maximumSize(10000),设置过期时间
场景2:元空间溢出 - 动态生成类
真实案例:RPC框架动态代理类过多
现象:Metaspace OOM,Full GC频繁
原因:Cglib动态生成大量代理类,元空间填满
解决:-XX:MaxMetaspaceSize调大,检查是否有类泄漏
场景3:大对象直接进老年代 - 数组/集合
真实案例:批量查询返回大List
现象:每次查询都触发Full GC
原因:一次查询返回10万条数据,大List直接进老年代
解决:分页查询,限制单页大小
场景4:CMS并发模式失败 - 常见
真实案例:CMS老年代碎片多
现象:日志显示"concurrent mode failure",退化为Full GC
原因:CMS用标记-清除,碎片多,大对象分配失败
解决:G1替代CMS,或-XX:+UseCMSCompactAtFullCollection
场景5:内存泄漏 - 静态集合
真实案例:静态Map未清理
现象:Full GC越来越频繁,老年代使用率只增不减
原因:
public static Map<Long, User> cache = new HashMap<>();
// 用户注销后没有remove,对象越积越多
解决:修复泄漏点,用WeakHashMap或手动清理
场景6:ThreadLocal内存泄漏
真实案例:Tomcat线程池复用
现象:重启应用后元空间增长,多次重启后OOM
原因:
ThreadLocal<User> userContext = new ThreadLocal<>();
// 线程结束前没有remove(),线程池复用导致对象泄漏
解决:finally块中必须remove()
场景7:显示调用System.gc()
真实案例:定时任务主动触发GC
现象:每小时准点Full GC
原因:
ScheduledExecutorService.scheduleAtFixedRate(() -> {
System.gc(); // 画蛇添足
}, 1, 1, TimeUnit.HOURS);
解决:删除代码,加上-XX:+DisableExplicitGC
🟡 G1 GC 特殊触发场景 #
场景8:IHOP阈值触发Mixed GC
G1参数:-XX:InitiatingHeapOccupancyPercent=45
现象:堆占用45%时开始并发标记,然后Mixed GC
原因:这是G1的正常机制,不是问题
场景9:G1退化Full GC(严重)
现象:G1发生Full GC,STW时间很长
原因:
- Mixed GC回收速度跟不上分配速度
- 巨型对象(Humongous Object)分配失败
- 晋升失败(Promotion Failure)
解决:调整-XX:ConcGCThreads,增大堆,或ZGC替代
🛠️ 如何定位GC触发原因? #
看GC日志关键字:
JDK8:
- [GC (Allocation Failure)] → Minor GC,Eden分配失败
- [Full GC (Ergonomics)] → 自适应策略触发
- [Full GC (System.gc())] → 显示调用System.gc()
- [Full GC (Metadata GC Threshold)] → 元空间触发
- [CMS: concurrent mode failure] → CMS失败退化Full GC
JDK11+:
- GC(Allocation Failure)
- GC(Metadata GC Threshold)
- GC(System.gc())
监控告警:
- Prometheus + Grafana:jvm_gc_pause_seconds_count
- 告警:Full GC频率>每小时1次,YGC停顿>500ms
💯 面试回答技巧:
先从理论分类(Minor/Major/Full)讲,然后重点讲生产环境真实案例,至少举3个以上你亲身经历(或准备好)的例子,比如:
- 缓存未设置大小导致的Full GC
- ThreadLocal未清理导致的泄漏
- 大对象直接分配导致的问题
这样面试官会觉得你有实战经验!
🔗 相关笔记 #
最后更新: 2026-05-13