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 存储 孵化器
端口号范围默认 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 发展主线 #

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