Java 虚拟机 (JVM) #

JVM 原理与调优面试核心知识点


📋 目录 #


内存模型 #

JVM 内存结构(Java 8+) #

JVM内存

堆内存 Heap

方法区 Metaspace

虚拟机栈 VM Stack

本地方法栈 Native Stack

程序计数器 PC Register

年轻代 Young

Eden区

Survivor0

Survivor1

老年代 Old

区域 作用 线程私有 是否GC
堆内存 存放对象实例
方法区 类信息、常量
虚拟机栈 方法调用、局部变量
本地方法栈 Native方法
程序计数器 字节码行号

堆内存分代模型 #

堆内存 (Heap)

年轻代 Young ~33%

Eden 8

Survivor0 1

Survivor1 1

老年代 Old ~67%

长期存活对象

比例: 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 Roots 类型:

  1. 虚拟机栈中引用的对象
  2. 方法区静态变量引用
  3. 方法区常量引用
  4. 本地方法栈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

Parallel Scavenge

ParNew

G1

Serial Old

Parallel Old

CMS

G1

ZGC

Shenandoah


Serial 收集器(串行) #

实现原理:

  • 单线程收集,暂停所有用户线程(Stop The World)
  • 年轻代用复制算法,老年代用标记-整理
  • 客户端模式默认收集器

优点:

  • 简单高效,没有线程交互开销
  • 单CPU场景比并发收集器更快

缺点:

  • 收集时暂停所有线程,不适合服务端

Parallel 收集器(并行) #

实现原理:

  • 多线程并行收集,仍然STW
  • 年轻代 Parallel Scavenge(复制),老年代 Parallel Old(标记-整理)
  • 目标是高吞吐量

参数:

-XX:+UseParallelGC
-XX:+UseParallelOldGC      # JDK 8+ 默认开启
-XX:ParallelGCThreads=N    # 并行线程数

适用场景:

  • 多核CPU,注重吞吐量
  • 后台计算、批处理任务

CMS 收集器(并发标记清除) #

目标: 低延迟,获取最短回收停顿时间

实现原理(四阶段):

初始标记 CMS-initial-mark

并发标记 CMS-concurrent-mark

重新标记 CMS-remark

并发清除 CMS-concurrent-sweep

STW 阶段:

  • 初始标记:仅标记GC Roots能直接关联到的对象 → 快
  • 重新标记:修正并发标记期间因用户程序运行导致标记变动 → 比初始标记稍长,远比对数整个堆扫描快

并发阶段:

  • 并发标记:和用户线程一起并发遍历标记对象 → 慢但不暂停
  • 并发清除:清除垃圾,和用户并发执行 → 慢但不暂停

特点:

  • 整体是并发收集,停顿时间短
  • 老年代用标记-清除算法,会产生内存碎片
  • 需要预留空间给并发运行时分配新对象

缺点:

  • 浮动垃圾:并发清除阶段用户程序还在分配新垃圾,只能下次GC回收
  • 内存碎片:标记清除产生碎片,需要整理
  • 对CPU敏感:并发阶段占用CPU时间

为什么CMS被移除?

  • G1/ZGC性能更好,解决了CMS内存碎片问题
  • JDK 14 正式移除 CMS

G1 收集器 Garbage-First #

面向服务端大堆,默认收集器(JDK 9+)⭐⭐⭐⭐⭐

核心设计思想 #

Region 分区:

G1 堆内存

Old 老年代区

O

O

O

O

Eden 伊甸园区

E

E

Survivor 幸存区

S

S

Humongous 大对象区

H

  • 整个堆分成多个等大 Region,每个 Region 大小 1MB ~ 32MB
  • 不再固定区分 Eden/Survivor/Old,任何 Region 都可以存放
  • 大对象(超过Region一半)直接放在连续Humongous Region
  • 垃圾回收优先回收垃圾多的Region,所以叫 Garbage-First

回收过程(四种阶段) #

年轻代满

年轻代GC(Evacuation)

堆超过阈值?
InitiatingHeapOccupancyPercent

并发标记

重新标记

混合回收(Mixed GC)

回收多个高收益Old Region

拷贝存活对象 → 空Region

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位)存储对象标记信息:

64 bits

18 位: 预留 unused

4 位: 标记位 marked

42 位: 对象实际地址 object address

finalizable 可终结

remapped 重映射

marked 标记

marked1 标记第二轮

优点:

  • 标记信息直接存在指针里,不需要额外空间存储
  • 读取屏障实现并发标记,不需要STW

2. 多重映射 Multi-mapping #

同一个对象物理页,映射到三个不同虚拟地址:

  • Address0 / Address1 / Address2
  • 用哪个地址表示当前标记颜色
  • 不需要扫描对象头就能知道标记

3. 读屏障 Load Barrier #

ZGC 使用读屏障实现染色指针的转发:

  • 用户读取对象引用时,经过读屏障处理
  • 根据染色位做相应转发
  • 整个标记过程完全并发,不需要STW

ZGC 回收过程 #

初始标记

STW 短暂停

并发标记

和用户线程一起跑

再标记

STW 短暂停

并发准备

初始转移

STW 短暂停

并发转移

复制存活对象到新页
清空旧页

所有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字节码
验证 字节码安全性
准备 为静态变量分配内存(零值)
解析 符号引用转直接引用
初始化 执行静态代码块、赋静态变量初值

类加载器 #

Custom ClassLoader
自定义类加载器

Application ClassLoader
应用类加载器

Extension ClassLoader
扩展类加载器

Bootstrap ClassLoader
启动类加载器

为什么要双亲委派?

  • 安全性: 避免自定义类替换核心类
  • 唯一性: 确保类只被加载一次

类加载时机 #

触发动作 说明
new 创建实例
反射 调用Class.forName()
子类加载 初始化父类
main 启动类

性能调优 #

调优目标 #

调优目标

吞吐量 ↔ 低延迟

内存占用 ↔ GC时间

响应速度 ↔ CPU利用率

调优步骤 #

监控分析

定位瓶颈

参数调整

验证效果

循环优化

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 问题排查 #

OOM发生

获取堆转储

MAT分析

问题定位

内存泄漏

修复泄漏点

内存不足

调整堆大小

常见泄漏原因:

  1. 静态集合未清理
  2. ThreadLocal 未 remove
  3. 数据库连接未关闭
  4. 监听器未注销
  5. 缓存未限制大小

JVM 常量池 #

类加载

引用

Class文件常量池

运行时常量池

字符串常量池

常量池 位置 内容
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 存储 孵化器
端口号范围默认 01024

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 发展主线 #

JDK 8

Serial/Parallel CMS

JDK 9

G1 默认

JDK 11

ZGC 诞生

JDK 14

CMS 移除

JDK 17

ZGC 成熟

JDK 21

分代ZGC


🎯 面试题汇总 #

基础 #

  1. JVM内存结构?

答:

JVM内存结构分为以下五个区域:

区域 作用 线程私有 是否GC 特点
堆(Heap) 存放对象实例和数组,GC主要区域 ❌ 共享 ✅ 是 最大一块,年轻代+老年代
方法区(Method Area) 存储类信息、常量、静态变量 ❌ 共享 ✅ 是 JDK8前叫永久代,8后叫元空间(Metaspace)
虚拟机栈(VM Stack) 方法调用栈帧,局部变量表、操作数栈 ✅ 私有 ❌ 否 每个线程一个,栈帧入栈出栈
本地方法栈(Native Stack) Native方法调用栈 ✅ 私有 ❌ 否 类似虚拟机栈,服务于native方法
程序计数器(PC Register) 当前线程执行的字节码行号指示器 ✅ 私有 ❌ 否 唯一个不会OOM的区域

JVM内存

堆内存 Heap

方法区 Metaspace

虚拟机栈 VM Stack

本地方法栈 Native Stack

程序计数器 PC Register

年轻代 Young

Eden区

Survivor0

Survivor1

老年代 Old

注意: JDK 8 中永久代被移除,改用元空间(Metaspace),使用本地内存,不在JVM堆中。


  1. 堆内存分代模型?

答:

堆内存分为年轻代老年代

堆内存 (Heap)

年轻代 Young ~33%

Eden 8

Survivor0 1

Survivor1 1

老年代 Old ~67%

长期存活对象

比例: 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

为什么分代?

  • 不同代对象生命周期不同,采用不同回收算法
  • 年轻代对象朝生夕死,用复制算法
  • 老年代对象存活时间长,用标记-整理算法

  1. GC Roots类型?

答:

GC Roots是垃圾回收的起点,从这些对象开始向下搜索,不可达的对象就是垃圾。

GC Roots包括:

类型 说明
虚拟机栈中引用的对象 栈帧中局部变量表引用的对象
方法区中静态变量引用的对象 static字段引用的对象
方法区中常量引用的对象 final常量引用的对象
本地方法栈中JNI引用的对象 Native方法引用的对象
活跃线程 当前正在运行的线程
同步锁(synchronized) 锁对象
JVM内部引用 Class对象、异常对象、系统类加载器等

GC Roots

栈中引用对象

静态变量引用

JNI引用

活跃线程

可达对象-保留

不可达对象

标记垃圾-回收

注意: GC Roots必须是堆外指向堆内的引用,堆内对象互相引用不算GC Roots。


  1. 四种引用类型区别?

答:

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 #

  1. 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

  1. 有哪些GC算法?标记清除/标记整理/复制算法区别?

答:

三种经典GC算法对比:

算法 原理 优点 缺点 适用场景
标记-清除 先标记垃圾,再统一清除 简单,不需要移动对象 内存碎片,分配大对象困难 老年代,对象存活率高
标记-整理 标记后,存活对象向一端移动,清除边界外 无内存碎片 需要移动对象,效率低 老年代,CMS remark后
复制 分为两块,只使用一块,回收时存活对象复制到另一块 无碎片,简单高效 需要额外空间,存活对象多则复制成本高 年轻代,朝生夕死

图解:

复制算法 Copying

From区 ┌───┬───┬───┐
│ ✓ │ ✗ │ ✓ │ → 复制到To区
└───┴───┴───┘

To区 ┌───┬───┬───┐
│ ✓ │ ✓ │ │ ← 存活对象复制
└───┴───┴───┘

标记-整理 Mark-Compact

┌───┬───┬───┬───┬───┐
│ ✓ │ ✗ │ ✓ │ ✗ │ ✗ │ ← 标记后
└───┴───┴───┴───┴───┘

┌───┬───┬───┬───┬───┐
│ ✓ │ ✓ │ │ │ │ ← 整理后无碎片!
└───┴───┴───┴───┴───┘

标记-清除 Mark-Sweep

┌───┬───┬───┬───┬───┐
│ ✓ │ ✗ │ ✓ │ ✗ │ ✗ │ ← 标记后
└───┴───┴───┴───┴───┘

┌───┬───┬───┬───┬───┐
│ ✓ │ │ ✓ │ │ │ ← 清除后有碎片!
└───┴───┴───┴───┴───┘

为什么年轻代用复制算法?

  • 年轻代98%对象朝生夕死,存活少,复制成本低
  • 没有内存碎片

为什么老年代不用复制算法?

  • 老年代对象存活率高,复制成本高
  • 没有额外空间浪费

  1. 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

  1. CMS收集器四阶段?为什么CMS被移除?

答:

CMS四个阶段:

初始标记
CMS-initial-mark

并发标记
CMS-concurrent-mark

重新标记
CMS-remark

并发清除
CMS-concurrent-sweep

阶段 是否STW 说明 耗时
初始标记 ✅ STW 仅标记GC Roots直接关联的对象 很快
并发标记 ❌ 并发 和用户线程一起,遍历整个对象图 慢,但不暂停
重新标记 ✅ STW 修正并发标记期间用户程序导致的标记变动 中等
并发清除 ❌ 并发 清除垃圾对象,和用户线程一起 慢,但不暂停

CMS为什么被移除?

  1. 内存碎片问题

    • 使用标记-清除算法,会产生内存碎片
    • 碎片过多时,分配大对象找不到连续空间
    • 可能提前触发Full GC
  2. 浮动垃圾

    • 并发清除阶段,用户线程还在运行,产生新垃圾
    • 这些新垃圾只能等下次GC回收
    • 需要预留空间给用户线程
  3. 对CPU资源敏感

    • 并发阶段占用CPU线程
    • 核心少的服务器影响吞吐量
  4. G1更优秀

    • G1解决了CMS的内存碎片问题
    • G1有可预测停顿模型
    • G1性能更好

JDK版本:

  • JDK 9:CMS被标记为Deprecate
  • JDK 14:CMS被完全移除

  1. G1收集器原理?Region分区优势?

答:

G1核心设计思想:Region分区

G1不再固定年轻代和老年代的物理边界,而是将堆分为多个大小相等的Region:

G1 堆内存

Old 老年代区

O

O

O

O

Eden 伊甸园区

E

E

Survivor 幸存区

S

S

Humongous 大对象区

H

Region特点:

  • 每个Region大小:1MB ~ 32MB,根据堆大小自动选择
  • 可以是Eden、Survivor、Old、Humongous中的任一种
  • 大对象(超过Region的一半)直接放在连续的Humongous Region

G1回收过程:

年轻代满

年轻代GC Evacuation

堆占用超过
IHOP阈值?

并发标记 Concurrent Mark

重新标记 Remark

混合回收 Mixed GC

选择多个高收益
Old Region回收

复制存活对象
到空Region

Region分区的优势:

  1. 不需要固定分代大小

    • 可根据应用动态调整年轻代和老年代大小
    • 更灵活
  2. 局部收集

    • 不需要每次都收集整个老年代
    • 只选择垃圾多的Region,收益高
  3. 内存整理

    • 复制算法,无内存碎片
    • 解决了CMS的碎片问题
  4. 可预测停顿

    • 根据历史数据,预测回收多少Region能满足停顿目标
    • 用户可设置-XX:MaxGCPauseMillis=200

  1. G1怎么实现可预测停顿?

答:

可预测停顿模型是G1最大特色:

用户可以通过参数设置期望的最大停顿时间:

-XX:MaxGCPauseMillis=200  # 默认200ms

G1如何实现:

  1. 收集停顿历史数据

    • G1持续跟踪每个Region的回收耗时
    • 建立数据库:回收这个Region需要多少时间,能回收多少垃圾
  2. 计算收益性价比

    • 每次GC前,根据历史数据预测
    • 选择"回收价值/耗时"最高的Region
    • 优先回收垃圾多、回收快的Region
  3. 控制收集范围

    • 不一次性回收所有Region
    • 根据停顿目标,选择合适数量的Region
    • 年轻代Region + 部分收益高的老年代Region = Mixed GC
  4. 动态调整

    • 如果某次GC超过了目标,下次减少收集Region数
    • 如果没达到,可以多收集一些

Remembered Set(记忆集)加速:

每个Region都有自己的Remembered Set,记录其他Region指向本Region的引用:

  • 不需要扫描全堆,只扫描Remembered Set
  • 加速Young GC和Mixed GC

注意: 可预测是"尽量满足",不是"绝对保证",如果存活对象太多还是可能超过目标。


  1. ZGC染色指针原理?为什么停顿这么短?

答:

ZGC的三个核心技术:

  1. 染色指针(Colored Pointers)
  2. 读屏障(Load Barrier)
  3. 多重映射(Multi-Mapping)

染色指针原理:

利用64位指针的高位存储对象标记信息(注意:64位指针实际只用到48位寻址):

64 bits

18 位: 未使用 unused

4 位: 标记位 marked

42 位: 实际对象地址

finalizable 可终结

remapped 重映射

marked0 标记0

marked1 标记1

4个标记位含义:

  • marked0/marked1:三色标记使用
  • remapped:对象是否被移动(重映射)
  • finalizable:是否可 finalize

为什么这样设计?

  • 标记信息直接在指针上,不需要在对象头存
  • 读取指针时通过读屏障处理,不需要STW

ZGC停顿为什么这么短?

ZGC阶段 是否STW 说明
初始标记 ✅ 短 仅标记GC Roots
并发标记 和用户线程并发
再标记 ✅ 极短 修正并发标记
并发准备 准备转移
初始转移 ✅ 极短 Roots重映射
并发转移 并发复制对象

所有STW阶段都和堆大小无关!

  • 只和GC Roots数量有关,时间很短(毫秒级)
  • 不论堆是8GB还是1TB,停顿都差不多

ZGC怎么做到并发转移?

  • 读屏障(Load Barrier):读取对象指针时,检查是否已被移动
  • 如果对象被移动了,指针指向新地址(指针"自愈")
  • 染色指针让对象移动也不需要修正所有引用

  1. 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 + 分代(性能更好)

  1. 什么是三色标记法?

答:

三色标记法是并发标记的基础算法:

将对象分为三种颜色:

颜色 含义 说明
白色 未标记 垃圾对象,最后会被回收
灰色 正在标记 自身已标记,但成员变量还没处理完
黑色 已标记 自身和所有成员变量都处理完,安全存活

标记过程:

GC Root黑色

对象A灰色

对象B黑色

对象C白色

对象D白色

步骤:

  1. GC Roots初始标记为黑色
  2. 从GC Roots开始,引用的对象标记为灰色
  3. 处理灰色对象,其引用的对象也变灰,自己变黑
  4. 直到没有灰色对象
  5. 剩下的白色对象就是不可达垃圾

三色标记的漏标问题:

并发标记时,用户线程可能修改引用关系,导致应该存活的对象被标记为白色(漏标):

漏标两个必要条件:

  1. 黑色对象指向白色对象(新增引用)
  2. 灰色对象不再指向白色对象(删除引用)

解决方案:

  • Incremental Update(增量更新):CMS使用,黑色对象新增引用时重新标记
  • SATB(Snapshot At The Beginning):G1使用,保存开始时的快照
  • 读屏障:ZGC使用,读取时检查并修正

  1. 如何优化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停顿时间
  • 吞吐量和延迟平衡

类加载 #

  1. 类加载过程?

答:

类加载全过程:

加载 Loading

链接 Linking

验证 Verification

准备 Preparation

解析 Resolution

初始化 Initialization

详细步骤:

阶段 说明 动作
加载 获取字节码 通过类名获取二进制流,在内存生成Class对象
验证 确保字节码安全 文件格式、元数据、字节码、符号引用验证
准备 分配内存赋零值 静态变量分配内存,设为初始零值(0/false/null)
解析 符号引用转直接引用 把常量池的符号引用替换为内存地址的直接引用
初始化 执行<clinit> 静态代码块执行,静态变量赋初始值

注意:

  • 准备阶段只赋零值:static int a = 1; → 准备阶段a=0,初始化阶段才=1
  • 初始化时,父类优先于子类初始化
  • 接口初始化不会触发父接口初始化

类初始化触发时机(有且只有):

  1. new对象
  2. 访问静态字段(非final)或静态方法
  3. Class.forName()反射
  4. 子类初始化时,先初始化父类
  5. 主类启动(有main方法)
  6. MethodHandle/JDK动态语言支持

  1. 双亲委派机制及破坏?

答:

双亲委派模型:

Custom ClassLoader
自定义类加载器

Application ClassLoader
应用类加载器

Extension ClassLoader
扩展类加载器

Bootstrap ClassLoader
启动类加载器

工作原理:

  1. 类加载器收到请求,先不自己加载
  2. 委派给父类加载器(向上委派)
  3. 父类加载不了,才自己加载(向下尝试)
  4. 父优先,保证基础类只加载一次

为什么要双亲委派?

  • 安全:防止自定义类替换核心类(如java.lang.String
  • 唯一:保证同一个类只被加载一次
  • 有序:类层次清晰

双亲委派的破坏:

破坏1:JDK 1.2之前,自定义类加载器直接重写loadClass()

  • 后来增加findClass()让用户重写,保持双亲委派

破坏2:线程上下文类加载器(Thread Context ClassLoader)

  • 解决SPI问题:JDBC/JNDI等,父类加载器需要加载子类的代码
  • 通过Thread.setContextClassLoader()设置上下文类加载器

破坏3:OSGi等模块化框架

  • 动态热部署,类加载器网状结构
  • 不严格按父子关系

  1. 类加载时机?

答:

主动引用(触发初始化):

场景 示例
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没有初始化!)

调优 #

  1. 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

  1. 如何排查OOM?

答:

OOM排查流程:

OOM发生

获取堆转储Heap Dump

MAT/JProfiler分析

问题定位

内存泄漏

修复泄漏点

内存不足

调整堆大小

大对象

优化代码

第一步:获取堆转储

# 参数配置自动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

内存泄漏常见场景:

  1. 静态集合未清理
  2. ThreadLocal未remove
  3. 数据库连接未关闭
  4. 监听器未注销
  5. 缓存未限制大小

  1. 有哪些监控工具?

答:

命令行工具(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

  1. 深拷贝与浅拷贝?

答:

浅拷贝 vs 深拷贝:

对比项 浅拷贝 深拷贝
对象本身 ✅ 复制 ✅ 复制
引用类型成员 ❌ 只复制引用,共享同一对象 ✅ 递归复制对象
修改影响 原对象和拷贝对象互相影响 互不影响

图解:

深拷贝 Deep Copy

Object A (拷贝)

ref 副本

Object B (原对象)

ref 原对象

浅拷贝 Shallow Copy

Object A (拷贝)

共享 ref 对象

Object B (原对象)

实现方式:

// 方式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版本特性 #

  1. 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();

  1. 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更全面)

  1. 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 {}

  1. 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已优化)

  1. 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的三个核心技术:

  1. 染色指针:指针高位存标记信息,不需要对象头
  2. 读屏障:读取指针时处理转发,并发转移
  3. 多重映射:同一物理页映射不同虚拟地址

适用场景:

  • 低延迟要求(交易系统、游戏)
  • 大堆(32GB+)
  • 云原生微服务
  • 对停顿敏感的应用

配置:

-XX:+UseZGC
-XX:ConcGCThreads=2

  1. 为什么移除CMS垃圾收集器?

答:

CMS(Concurrent Mark Sweep)在JDK9标记为Deprecated,JDK14正式移除:

移除原因:

  1. 内存碎片问题

    • 标记-清除算法,产生内存碎片
    • 碎片过多时,大对象分配失败
    • 提前触发Full GC
  2. 浮动垃圾

    • 并发清除阶段,用户线程还在运行产生新垃圾
    • 新垃圾只能等下次GC回收
    • 需要预留空间给用户线程分配对象
  3. CPU资源消耗

    • 并发阶段占用CPU线程
    • 核心少的服务器,吞吐量下降明显
  4. G1更优秀

    • G1解决了内存碎片问题(复制算法)
    • G1有可预测停顿模型
    • G1性能更好,功能更全
    • G1从JDK9开始就是默认收集器
  5. 维护成本高

    • 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正式可用

  1. 实际工作中有哪些情况会触发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个以上你亲身经历(或准备好)的例子,比如:

  1. 缓存未设置大小导致的Full GC
  2. ThreadLocal未清理导致的泄漏
  3. 大对象直接分配导致的问题

这样面试官会觉得你有实战经验!


🔗 相关笔记 #


最后更新: 2026-05-13