一、核心前置概念
1. 什么是 GC(Garbage Collection)
GC = 垃圾回收,指 JVM 自动识别不再被引用的对象,释放堆内存,避免手动管理内存(区别 C/C++ 手动 malloc/free)。 GC 核心解决两个问题:
- 怎么判断对象是垃圾(存活判定)
- 怎么回收垃圾(回收算法、收集器、内存分代)
2. JVM 内存分区与 GC 范围
JVM 运行时数据区分为 5 块,只有堆、元空间(永久代)存在 GC:
- 程序计数器:无 GC,线程私有,生命周期随线程;
- 虚拟机栈 / 本地方法栈:栈帧自动出栈释放,不依赖 GC;
- 元空间(Metaspace,JDK8+):存放类、常量、静态变量,回收条件苛刻;
- 堆 Heap:GC 主战场,所有 new 创建的对象存放于此,分代管理。
3. 堆内存分代模型(主流分代收集思想)
绝大多数收集器将堆划分为两大块,依据对象存活时间差异化回收:
- 新生代 Young Gen:存放短期存活对象,占堆 1/3
- Eden 区:80% 新生代,绝大多数对象诞生于此
- Survivor0 / Survivor1(S0、S1,幸存区):各 10% 新生代,永远一块空、一块存存活对象
- 老年代 Old Gen:存放长期存活对象,占堆 2/3 对象晋升规则:新生代对象熬过多次 Minor GC(默认 15 次)进入老年代;大对象直接进老年代。
GC 分类
- Minor GC(年轻代 GC):只回收新生代,频率高、速度快,触发后 STW 很短
- Major GC:仅回收老年代,很少单独触发
- Full GC:整堆回收(新生代 + 老年代 + 元空间),STW 极长,业务需极力避免
二、对象存活判定:两种垃圾标记算法
GC 第一步:标记哪些对象是垃圾,核心难点是循环引用。
1. 引用计数法
逻辑:每个对象维护计数器,引用 + 1,引用失效 - 1;计数 = 0 即为垃圾。 缺陷:无法解决对象循环引用(A 引用 B,B 引用 A,计数器永远不为 0),因此 JVM 弃用。
2. 可达性分析算法
核心逻辑
从GC Roots(根对象)向下遍历,能遍历到的对象判定为存活,不可达为垃圾。
GC Roots 包含哪些对象
- 虚拟机栈中局部变量引用的对象
- 本地方法栈 JNI 引用的对象
- 静态变量、常量引用的对象(元空间)
- 运行中线程、锁对象、Class 对象等
四种引用
- 强引用:
Object o = new Object();GC 永不回收,内存溢出根源 - 软引用 SoftReference:内存不足时才回收,适合缓存
- 弱引用 WeakReference:下次 GC 必回收,ThreadLocalMap 底层使用
- 虚引用 PhantomReference:仅做回收通知,无法获取对象,用于堆外内存释放
三、三大垃圾回收基础算法
标记完成后,使用以下算法清理内存,收集器都是组合使用。
1. 标记 - 清除 Mark-Sweep
流程:1. 标记垃圾 → 2. 直接清除垃圾 优点:逻辑简单,无需移动对象 缺点:内存产生大量内存碎片;分配大对象时容易触发 Full GC
2. 复制 Copying
流程:将存活对象复制到空白内存区,清空原区域 新生代 Eden+S0/S1 就是此算法: Eden 满了,存活对象复制到空 Survivor,清空 Eden;下次 GC 交换两块 Survivor。 优点:无内存碎片,分配内存只需指针碰撞,速度极快 缺点:需要双倍内存空间,不适合大对象(老年代不用)
3. 标记 - 整理 Mark-Compact
流程:1. 标记垃圾 → 2. 所有存活对象向内存一端移动,边界外全部清空 优点:无内存碎片,适合老年代长期对象 缺点:移动对象需要 STW,开销比标记清除大
算法总结
- 新生代:复制算法(对象存活率低,复制成本小)
- 老年代:标记清除 / 标记整理(对象存活率高,复制代价太大)
四、分代回收完整流程
- 新对象全部分配在 Eden 区;
- Eden 空间耗尽,触发 Minor GC;
- 通过可达性分析标记 Eden、使用中的 Survivor 存活对象;
- 将存活对象复制到空白 Survivor 区,对象年龄 + 1;
- Eden、原 Survivor 全部清空,交换两块 Survivor 角色;
- 对象年龄达到阈值(-XX:MaxTenuringThreshold=15),直接晋升老年代;
- 若 Survivor 放不下存活对象,存活对象直接晋升老年代(分配担保);
- Minor GC 前会检查老年代可用空间,若不足直接触发 Full GC。
五、STW(Stop-The-World)关键概念
- 含义:GC 标记、复制、整理阶段,所有用户业务线程全部暂停,只运行 GC 线程;
- 弊端:停顿时间过长会导致接口超时、服务卡顿;
- 收集器迭代核心目标:降低 STW 时长;
- 补充:CMS、G1、ZGC 使用并发标记,仅初始标记、重新标记阶段短暂 STW。
六、主流垃圾收集器(分代 / 分区 / 低延迟)
1. Serial 串行收集器(单线程,客户端小程序)
- Serial Young:新生代复制,Serial Old:老年代标记整理
- 全程单线程 GC,全部 STW,单核机器简单场景使用,生产极少用
2. Parallel 并行收集器(JDK 默认吞吐量收集器)
- Parallel Scavenge(新生代)+ Parallel Old(老年代)
- GC 使用多线程并行回收,用户线程全程暂停;
- 目标:最大化吞吐量(运行代码时间 / 总运行时间);
- 适合后台批处理、数据同步,不追求低停顿。
3. CMS 并发标记清除(JDK8 经典老年代收集器,JDK14 废弃)
第一款并发收集器,目标低延迟,老年代专用,搭配 ParNew 新生代。
CMS 四大阶段
- 初始标记(STW):仅标记 GC Roots 直接关联对象,停顿极短
- 并发标记(无 STW):GC 线程和业务线程同时运行,遍历全部对象
- 重新标记(STW):修正并发阶段业务线程新增的垃圾,短暂停顿
- 并发清除(无 STW):并发清理垃圾
CMS 致命缺陷
- 使用标记清除,产生大量内存碎片,频繁 Full GC;
- 并发阶段业务线程持续创建对象,可能出现并发失败,退化为 Serial Old 单线程 Full GC,停顿暴涨;
- 浮动垃圾:并发清除阶段新产生对象,本次无法回收,只能等下次 GC。
4. G1(Garbage-First,JDK9 默认,分区式收集器)
打破传统分代,将整个堆划分为多个大小相等的独立 Region(1M~32M); 每个 Region 可动态充当 Eden、Survivor、Old,不再固定新生代 / 老年代边界。
核心优势
- 可自定义预期停顿时间(-XX:MaxGCPauseMillis),优先回收垃圾最多的 Region;
- 整合复制 + 标记整理,全程无内存碎片;
- 兼顾吞吐量与低延迟,替代 CMS,中小型生产通用。
G1 五大阶段
初始标记 (STW) → 并发标记 → 最终标记 (STW) → 筛选回收 (STW)
5. ZGC / Shenandoah(超低延迟收集器,JDK11 + 企业级)
面向超大堆(TB 级),STW 停顿控制在10ms 以内; 使用读屏障、染色指针、负载转发,几乎全程并发,大型高并发服务首选。
七、内存分配与特殊规则
- 指针碰撞分配:新生代内存连续,分配对象仅移动指针,速度快;
- TLAB 线程本地分配缓冲区:每个线程私有一小块 Eden,避免多线程分配锁竞争;
- 大对象直接晋升老年代:超过 - XX:PretenureSizeThreshold 的对象跳过新生代,防止大量复制;
- 动态年龄判断:Survivor 中相同年龄对象总大小超过 Survivor 一半,该年龄及以上对象直接晋升老年代,无需等到 15 岁。
八、常见 GC 优化方向
- 避免 Full GC:控制大对象、合理设置新生代大小、避免内存泄漏;
- 选择合适收集器:批处理用 Parallel,高并发接口用 G1/ZGC;
- 调优分代比例:Xms/Xmx 固定堆大小,避免运行时扩容缩容;
- 排查内存泄漏:静态集合长期持有对象、ThreadLocal 未清理、IO 流未关闭;
- 开启 GC 日志,分析停顿、内存升降、晋升频率。
九、整体总结
- GC 本质:自动识别不可达对象,释放堆内存,核心算法是可达性分析;
- 堆按对象生命周期分代,新生代复制算法、老年代标记整理 / 清除;
- GC 存在 STW,收集器演进路线:串行→并行→并发 CMS→分区 G1→超低延迟 ZGC;
- Minor GC 高频轻量,Full GC 代价极高,线上业务核心优化目标就是减少 Full GC。