1. 垃圾回收基础理论
问题:什么是垃圾回收?为什么需要垃圾回收?
详细解答:
垃圾回收定义
垃圾回收(Garbage Collection,GC)是自动内存管理机制,负责识别和回收不再使用的对象所占用的内存空间。
为什么需要GC
手动内存管理的问题:
- C/C++需要手动malloc/free,容易出现内存泄漏
- 野指针问题导致程序崩溃
- 双重释放造成内存损坏
- 开发效率低,需要时刻关注内存释放
GC的优势:
- 自动回收不再使用的对象
- 避免内存泄漏和野指针
- 提高开发效率
- 程序更加健壮可靠
GC的核心问题
三个基本问题:
- 哪些内存需要回收? → 对象是否存活判断
- 什么时候回收? → GC触发条件
- 如何回收? → 垃圾回收算法
2. 对象存活判断算法
问题:如何判断一个对象是否可以被回收?
详细解答:
引用计数法(Reference Counting)
原理:
对象添加引用计数器
引用+1:被引用时
引用-1:引用失效时
引用=0:可回收
优点:
- 实现简单
- 判定效率高
- 实时性好(引用计数为0立即回收)
致命缺陷:循环引用
java
public class CircularReference {
public Object instance = null;
public static void main(String[] args) {
CircularReference obj1 = new CircularReference();
CircularReference obj2 = new CircularReference();
// 循环引用
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
// 两个对象互相引用,引用计数永远不为0
// 但实际上已经无法访问,造成内存泄漏
}
}
Python的解决方案:
- 使用引用计数 + 标记清除解决循环引用
- JVM没有采用此方案
可达性分析算法(Reachability Analysis)
原理:
从GC Roots对象作为起点向下搜索
搜索路径称为引用链(Reference Chain)
对象到GC Roots没有任何引用链相连 → 不可达 → 可回收
GC Roots对象包括:
- 虚拟机栈中引用的对象
java
public void method() {
Object obj = new Object(); // obj是GC Root
}
- 方法区中类静态属性引用的对象
java
public class Test {
public static Object staticObj = new Object(); // staticObj是GC Root
}
- 方法区中常量引用的对象
java
public class Test {
public static final Object CONST_OBJ = new Object(); // CONST_OBJ是GC Root
}
- 本地方法栈中JNI引用的对象
java
native void nativeMethod(); // native方法中引用的对象
- JVM内部引用
- 基本类型对应的Class对象
- 异常对象
- 系统类加载器
-
synchronized持有的对象
-
JMXBean、JVMTI中注册的回调、本地代码缓存等
可达性分析示例:
GC Roots
├─> Object A ──> Object C
├─> Object B ──> Object D ──> Object E
│
└─> Object F
Object G <──> Object H (互相引用但不可达)
结论:
- A, B, C, D, E, F 可达,存活
- G, H 不可达,可回收
引用类型详解
强引用(Strong Reference)
java
Object obj = new Object(); // 强引用
// 只要强引用存在,永远不会被回收
// 宁可OOM也不回收
软引用(Soft Reference)
java
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
// 内存充足:不回收
// 内存不足:回收(OOM前)
// 应用场景:缓存
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
弱引用(Weak Reference)
java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 无论内存是否充足,GC时一定回收
// 生命周期:下次GC前
// 应用场景:WeakHashMap
WeakHashMap<Key, Value> cache = new WeakHashMap<>();
虚引用(Phantom Reference)
java
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 无法通过虚引用获取对象
// 唯一目的:对象被回收时收到系统通知
// 应用场景:堆外内存回收(DirectByteBuffer)
引用强度比较:
强引用 > 软引用 > 弱引用 > 虚引用
对象的自我拯救
finalize()方法机制:
java
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("I am still alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
// 自我拯救:重新建立引用
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 第一次拯救成功
SAVE_HOOK = null;
System.gc();
Thread.sleep(500); // 等待finalize执行
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive(); // 输出:I am still alive!
} else {
System.out.println("I am dead!");
}
// 第二次拯救失败(finalize只执行一次)
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("I am dead!"); // 输出这个
}
}
}
finalize()的问题:
- 执行时间不确定(低优先级Finalizer线程)
- 性能开销大
- 只执行一次
- 可能导致对象复活
架构师建议:
- 避免使用finalize()
- 使用try-finally或try-with-resources
- JDK 9引入Cleaner机制替代finalize()
3. 垃圾回收算法详解
问题:JVM中有哪些垃圾回收算法?各有什么优缺点?
详细解答:
标记-清除算法(Mark-Sweep)
工作流程:
1. 标记阶段:标记所有需要回收的对象
2. 清除阶段:统一回收被标记的对象
示意图:
回收前:[对象A][对象B][对象C][对象D][对象E]
标记后:[对象A][×对象B][对象C][×对象D][对象E]
清除后:[对象A][ ][对象C][ ][对象E]
优点:
- 实现简单
- 不需要移动对象
缺点:
- 效率问题:标记和清除效率都不高
- 空间问题:产生大量内存碎片
应用场景:
- CMS收集器的老年代回收
内存碎片问题示例:
java
// 假设需要分配连续100MB内存
byte[] largeArray = new byte[100 * 1024 * 1024];
// 虽然总空闲内存>100MB,但没有连续的100MB空间
// 导致分配失败,触发Full GC或OOM
标记-复制算法(Mark-Copy)
工作流程:
1. 将内存分为两块:From区和To区
2. 使用From区分配对象
3. GC时将From区存活对象复制到To区
4. 清空From区
5. 交换From和To的角色
示意图:
From区:[A][B][C][D][E] To区:[空]
↓ GC(B、D为垃圾)
From区:[空] To区:[A][C][E]
HotSpot的Eden + Survivor实现:
新生代分配:Eden:Survivor0:Survivor1 = 8:1:1
正常情况:
Eden + Survivor0 → Survivor1(存活对象<10%)
极端情况:
存活对象>Survivor容量 → 老年代担保分配
优点:
- 实现简单
- 运行高效
- 没有内存碎片
缺点:
- 空间浪费:可用内存缩小为原来的一半
- 存活率高时效率降低(需要复制大量对象)
应用场景:
- 新生代回收(对象存活率低,约10%)
代码示例:
java
// 新生代对象分配
public class YoungGenAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1 = new byte[2 * _1MB]; // Eden
byte[] allocation2 = new byte[2 * _1MB]; // Eden
byte[] allocation3 = new byte[2 * _1MB]; // Eden
// Eden空间不足,触发Minor GC
// allocation1、2、3晋升到老年代
byte[] allocation4 = new byte[4 * _1MB];
}
}
标记-整理算法(Mark-Compact)
工作流程:
1. 标记阶段:标记存活对象
2. 整理阶段:让所有存活对象向内存一端移动
3. 清理阶段:清理边界外的内存
示意图:
标记后:[A][×B][C][×D][E][×F]
整理后:[A][C][E][ ]
↑存活对象 ↑可分配空间
两种实现策略:
1. Move策略(移动存活对象)
java
// 伪代码
for (Object obj : liveObjects) {
moveToCompactArea(obj);
updateReferences(obj); // 更新所有引用
}
2. Slide策略(滑动压缩)
java
// 伪代码
// 三次扫描:
// 1. 计算新地址
// 2. 更新引用
// 3. 移动对象
优点:
- 没有内存碎片
- 空间利用率高
- 适合老年代(存活率高)
缺点:
- 效率问题:需要移动大量对象并更新引用
- 暂停时间长(Stop The World)
应用场景:
- Serial Old收集器
- Parallel Old收集器
分代收集理论
分代假说(Generational Hypothesis):
-
弱分代假说:
- 绝大多数对象都是朝生夕灭
- 98%的对象在第一次GC后被回收
-
强分代假说:
- 熬过多次GC的对象越难消亡
- 长时间存活的对象生命周期会更长
-
跨代引用假说:
- 跨代引用相对于同代引用占极少数
- 存在互相引用关系的对象倾向于同时生存或消亡
分代设计:
堆内存
├── 新生代(Young Generation)
│ ├── Eden区(80%)
│ ├── Survivor0区(10%)
│ └── Survivor1区(10%)
└── 老年代(Old Generation)
回收策略:
Minor GC(新生代GC):
- 触发条件:Eden区满
- 回收算法:复制算法
- 频率:高(秒级)
- 停顿时间:短(毫秒级)
Major GC(老年代GC):
- 触发条件:老年代满或晋升失败
- 回收算法:标记-清除或标记-整理
- 频率:低(分钟-小时级)
- 停顿时间:长(可能达到秒级)
Full GC(全堆GC):
- 触发条件:
- 老年代空间不足
- 元空间不足
- System.gc()调用
- CMS GC出现promotion failed、concurrent mode failure
- 回收范围:新生代+老年代+元空间
- 停顿时间:最长
对象晋升规则:
java
1. 长期存活对象进入老年代
-XX:MaxTenuringThreshold=15 // 默认15次
2. 大对象直接进入老年代
-XX:PretenureSizeThreshold=1048576 // 1MB
3. 动态年龄判定
// Survivor空间中相同年龄所有对象大小总和 > Survivor空间一半
// 年龄>=该年龄的对象直接进入老年代
4. 空间分配担保
// Minor GC前检查老年代最大连续空间 > 新生代所有对象总大小
// 是:安全执行Minor GC
// 否:Full GC
架构师实战经验:
分代收集优化要点:
- 根据对象生命周期特征调整新生代大小
- 合理设置晋升阈值避免频繁Full GC
- 大对象使用对象池或直接分配到老年代
- 监控晋升速率评估内存配置合理性
4. 垃圾收集器详解
问题:JVM有哪些垃圾收集器?各有什么特点和适用场景?
详细解答:
收集器总览
新生代收集器:
- Serial
- ParNew
- Parallel Scavenge
老年代收集器:
- Serial Old
- Parallel Old
- CMS
全堆收集器:
- G1
- ZGC(JDK 11)
- Shenandoah(JDK 12)
Serial / Serial Old收集器
特点:
- 单线程收集器
- 收集时必须暂停所有工作线程(Stop The World)
- 简单高效(单线程下没有线程交互开销)
工作流程:
用户线程 → [暂停] → Serial GC → [继续]
↓
单线程回收
参数配置:
bash
-XX:+UseSerialGC # 新生代Serial + 老年代Serial Old
适用场景:
- Client模式(桌面应用)
- 单核CPU或内存较小的环境
- 对停顿时间不敏感的应用
ParNew收集器
特点:
- Serial的多线程版本
- 新生代并行,老年代串行
- 与CMS配合使用
工作流程:
用户线程 → [暂停] → ParNew GC(多线程) → [继续]
参数配置:
bash
-XX:+UseParNewGC # 使用ParNew
-XX:ParallelGCThreads=4 # GC线程数(通常=CPU核心数)
线程数配置建议:
CPU核心数 <= 8:GC线程数 = CPU核心数
CPU核心数 > 8:GC线程数 = 3 + (5 * CPU核心数 / 8)
适用场景:
- 多核CPU环境
- 配合CMS使用
Parallel Scavenge / Parallel Old收集器
特点:
- 吞吐量优先收集器
- 新生代和老年代都是并行回收
- 自适应调节策略(GC Ergonomics)
关键参数:
bash
-XX:+UseParallelGC # 新生代Parallel Scavenge
-XX:+UseParallelOldGC # 老年代Parallel Old
-XX:MaxGCPauseMillis=200 # 最大停顿时间(毫秒)
-XX:GCTimeRatio=99 # 吞吐量大小(默认99,即1%时间GC)
-XX:+UseAdaptiveSizePolicy # 自适应调节策略
吞吐量计算:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)
例如:
运行100分钟,GC 1分钟
吞吐量 = 100 / (100 + 1) = 99%
自适应策略:
java
// JVM自动调整:
- 新生代大小
- Eden与Survivor比例
- 晋升老年代对象年龄阈值
// 目标:在停顿时间和吞吐量之间找到最优解
适用场景:
- 后台计算任务(批处理、科学计算)
- 不需要太多交互的应用
- 对停顿时间不敏感但要求高吞吐量
CMS收集器(Concurrent Mark Sweep)
设计目标:
- 获取最短停顿时间
- 互联网站或B/S系统的服务端
工作流程(四个阶段):
1. 初始标记(Initial Mark)- STW
标记GC Roots直接关联的对象
速度快,停顿时间短
2. 并发标记(Concurrent Mark)- 并发
从GC Roots遍历整个对象图
与用户线程并发执行
时间最长但不停顿
3. 重新标记(Remark)- STW
修正并发标记期间变动的对象标记记录
使用增量更新算法
停顿时间略长于初始标记
4. 并发清除(Concurrent Sweep)- 并发
清除死亡对象
与用户线程并发执行
时间线:
用户线程: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
CMS GC: ║ ▒▒▒▒▒▒ ║ ▒▒▒▒
初始 并发标记 重新 并发清除
标记 标记
║ = STW停顿
▒ = 并发执行
参数配置:
bash
-XX:+UseConcMarkSweepGC # 使用CMS
-XX:CMSInitiatingOccupancyFraction=70 # 触发CMS的老年代占用阈值
-XX:+UseCMSInitiatingOccupancyOnly # 只使用设定的阈值
-XX:+CMSScavengeBeforeRemark # 重新标记前进行一次Minor GC
-XX:+UseCMSCompactAtFullCollection # Full GC时进行碎片整理
-XX:CMSFullGCsBeforeCompaction=5 # 多少次Full GC后整理一次
优点:
- 并发收集
- 低停顿
缺点:
1. CPU资源敏感
默认GC线程数 = (CPU核心数 + 3) / 4
4核CPU:1个GC线程,占用25% CPU
2核CPU:1个GC线程,占用50% CPU(影响严重)
2. 无法处理浮动垃圾(Floating Garbage)
java
// 并发标记阶段产生的新垃圾
Object obj = new Object(); // 并发标记开始前存在
obj = null; // 并发标记期间变为垃圾
// 这部分垃圾要等下次GC才能回收
3. 内存碎片问题
使用标记-清除算法
产生大量内存碎片
可能导致提前触发Full GC
4. Concurrent Mode Failure
触发原因:
- 并发清除期间,老年代空间不足以容纳晋升对象
- 预留空间不足(CMSInitiatingOccupancyFraction设置过高)
后果:
- 启用Serial Old收集器进行Full GC
- 停顿时间大幅增加
解决方案:
- 降低CMSInitiatingOccupancyFraction值
- 增加老年代大小
适用场景:
- 重视响应速度的应用
- 互联网网站、B/S系统
- 不能容忍长时间停顿的服务
G1收集器(Garbage First)
设计目标:
- 在延迟可控的情况下获得尽可能高的吞吐量
- 替代CMS收集器
核心概念:Region
堆内存划分为多个大小相等的Region(1-32MB)
Region类型:
- Eden区
- Survivor区
- Old区
- Humongous区(大对象,>=Region大小的50%)
工作流程:
1. 初始标记(Initial Mark)- STW
标记GC Roots直接关联的对象
借用Minor GC的暂停
2. 并发标记(Concurrent Mark)- 并发
遍历对象图
使用SATB(Snapshot-At-The-Beginning)算法
3. 最终标记(Final Mark)- STW
处理SATB缓冲区
4. 筛选回收(Live Data Counting and Evacuation)- STW
根据停顿时间目标选择回收Region
将选中Region的存活对象复制到空Region
回收旧Region空间
关键技术:
1. Remembered Set(记忆集)
java
// 记录Region之间的引用关系
// 避免全堆扫描
// 每个Region维护一个RSet
class Region {
RememberedSet rset; // 记录哪些Region引用了本Region的对象
}
// Minor GC时只需扫描:
// - Eden区
// - Survivor区
// - RSet记录的引用Region
2. Collection Set(回收集合)
记录要被回收的Region集合
根据停顿时间目标动态选择
优先回收垃圾最多的Region(Garbage First)
3. 停顿预测模型
java
// 基于历史数据预测回收时间
// 动态选择回收Region数量
预测因素:
- 每个Region的垃圾占比
- 历史回收耗时
- 复制存活对象的耗时
参数配置:
bash
-XX:+UseG1GC # 使用G1
-XX:MaxGCPauseMillis=200 # 最大停顿时间目标
-XX:G1HeapRegionSize=16m # Region大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发GC的堆占用阈值
-XX:G1NewSizePercent=5 # 新生代最小占比
-XX:G1MaxNewSizePercent=60 # 新生代最大占比
-XX:ParallelGCThreads=8 # 并行GC线程数
-XX:ConcGCThreads=2 # 并发GC线程数
Mixed GC详解:
触发条件:
1. 并发标记完成
2. 老年代占用达到阈值
回收范围:
- 整个新生代
- 部分老年代Region
选择策略:
根据停顿时间目标和垃圾占比选择最值得回收的Region
优点:
- 可预测的停顿时间
- 没有内存碎片(复制算法)
- 并行与并发结合
- 分代收集但不需要连续空间
缺点:
- 内存占用高(RSet占堆内存约10%-20%)
- 执行负载高(写屏障维护RSet)
- 小堆(<4G)性能可能不如CMS
适用场景:
- 大堆内存(>4G)
- 需要可预测停顿时间
- 替代CMS的生产环境
ZGC收集器(JDK 11+)
设计目标:
- 停顿时间不超过10ms
- 支持TB级堆内存
- 停顿时间不随堆大小增加而增加
核心技术:
1. 着色指针(Colored Pointer)
64位指针布局:
[18位未使用][1位Finalizable][1位Remapped]
[1位Marked1][1位Marked0][42位对象地址]
通过指针中的标志位标记对象状态
2. 读屏障(Load Barrier)
java
// 每次从堆中读取对象引用时,检查并修复指针
Object obj = object.field;
// 读屏障检查指针状态
// 必要时进行重新映射
3. 并发整理
使用转发表(Forwarding Table)
实现对象移动的并发
参数配置:
bash
-XX:+UseZGC # 使用ZGC
-XX:ZCollectionInterval=120 # GC间隔(秒)
-XX:ZAllocationSpikeTolerance=2 # 分配尖峰容忍度
适用场景:
- 大内存应用(>100G)
- 要求极低延迟(<10ms)
- JDK 11及以上版本
架构师选择建议:
| 场景 | 推荐收集器 | 理由 |
|---|---|---|
| 小堆(<2G)低延迟 | ParNew+CMS | 成熟稳定 |
| 中大堆(4-64G) | G1 | 可预测停顿 |
| 超大堆(>64G)极低延迟 | ZGC | 停顿时间<10ms |
| 批处理高吞吐量 | Parallel | 吞吐量最高 |