新生代和老年代的 GC 算法有哪些?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
- 基础掌握度:面试官不仅仅是想知道你背了几种算法名字,更是想看你能否准确说出新生代和老年代各自使用的 GC 算法,以及背后的设计考量。
- 原理理解深度:能否解释清楚每种算法的工作原理、优缺点,以及为什么 HotSpot 选择"分代收集"这个策略,而不是所有区域用同一种算法。
- 实践关联:是否了解不同垃圾收集器(Serial、ParNew、Parallel Scavenge、CMS、G1 等)分别用了哪种算法,能否将理论和实际联系起来。
核心答案
直接上结论,用一张表说清楚:
| 分代 | GC 算法 | 核心思路 | 对应的垃圾收集器 |
|---|---|---|---|
| 新生代 | 复制算法(Copying) | 将存活对象复制到另一块空间 | Serial、ParNew、Parallel Scavenge |
| 老年代 | 标记-清除(Mark-Sweep) | 标记垃圾,直接清除 | CMS |
| 老年代 | 标记-整理(Mark-Compact) | 标记存活对象,整理到一端 | Serial Old、Parallel Old |
| 整堆 | 分区复制 + 标记-整理 | 分 Region 灵活处理 | G1、ZGC |
注意:新生代基本只用复制算法,而老年代根据收集器的不同,会选用标记-清除或标记-整理。
深度解析
一、为什么需要分代?
在讲具体算法之前,得先理解一个前提——为什么要把堆分成新生代和老年代?
研究发现,Java 系统中绝大多数对象都是"朝生夕死"的,活不过一轮 GC。但也有少部分对象是长时间存活的(比如缓存、连接池)。这两类对象的生存特征完全不同,如果用同一种算法处理,效率会很低。
所以 HotSpot 把堆分成了新生代和老年代,让不同区域各用最合适的算法,这就是 "分代收集" 的核心思想。
上图展示了 JVM 堆内存的经典布局:
- 新生代:分为
Eden区和两个Survivor区(S0、S1),默认比例8:1:1。新对象优先在Eden区分配。 - 老年代:存放经过多次 GC 仍然存活的长寿对象,默认占堆内存的
2/3。
二、新生代——复制算法
新生代的 GC 叫 Minor GC(也叫 Young GC),使用的是 复制算法。
工作流程
- 新对象分配在
Eden区 - 当
Eden区满了,触发 Minor GC - GC 从
GC Roots开始,标记所有存活对象 - 将
Eden和当前使用的Survivor(From)中的存活对象,复制 到另一个空的Survivor(To) - 清空
Eden和 From 区 - From 和 To 角色互换
上面的图示展示了复制算法的核心过程:
- GC 前:
Eden区和From区中有大量对象,其中只有少部分是存活的(用█表示),其余都是垃圾 - GC 后:所有存活对象被复制到
To区,Eden和From被整体清空,然后From和To的角色互换 - 关键点:不需要逐个清理垃圾,直接把存活对象搬走,原来的空间一把清空,效率非常高
为什么新生代适合复制算法?
因为新生代有个特点——98% 的对象都是朝生夕死的。每次 GC 只有极少量对象存活,需要复制的量很小。而且复制算法不需要考虑碎片问题,存活对象被集中放到 Survivor 区,空间天然连续。
代价是什么?就是空间浪费。Eden:S0:S1 = 8:1:1,始终有一块 Survivor 是空的,也就是浪费了大约 10% 的新生代空间。但这笔账很划算,因为避免了内存碎片。
对象什么时候晋升老年代?
不是所有对象都永远待在新生代,以下情况会晋升到老年代:
- 年龄达到阈值:每经历一次 Minor GC 且存活,年龄 +1,默认达到 15 岁(
-XX:MaxTenuringThreshold)就晋升 - 大对象直接进入老年代:超过
-XX:PretenureSizeThreshold的对象直接在老年代分配 - 动态年龄判断:如果
Survivor区中相同年龄的所有对象大小总和超过Survivor空间的一半,年龄 >= 该年龄的对象直接晋升
三、老年代——标记-清除与标记-整理
老年代的 GC 叫 Major GC(有时也叫 Full GC,严格来说 Full GC 是清理整个堆),根据垃圾收集器的不同,会使用不同的算法。
1. 标记-清除算法(Mark-Sweep)
这是最基础的算法,CMS 收集器用的就是它。
上图的流程分两步:
- 标记阶段:从
GC Roots开始遍历,标记所有可达的存活对象。图中█表示存活对象,░表示已被标记为垃圾的对象 - 清除阶段:直接回收垃圾对象占用的空间。注意看清除后的内存布局——存活对象之间出现了大量不连续的空闲区域,这就是 内存碎片 问题
优点:速度快,不需要移动对象。缺点:会产生内存碎片,后续分配大对象时可能找不到足够的连续空间,触发提前 GC。
2. 标记-整理算法(Mark-Compact)
Serial Old、Parallel Old 用的是这个算法。
- 标记阶段:和标记-清除一样,先标记所有存活对象
- 整理阶段:将所有存活对象向内存的一端移动,然后直接清理边界以外的空间。整理后的内存非常整齐,没有碎片
优点:没有内存碎片。缺点:移动对象需要更新所有引用,耗时比标记-清除更长。老年代对象多,移动的开销不小。
3. 两种算法怎么选?
| 对比维度 | 标记-清除 | 标记-整理 |
|---|---|---|
| 是否移动对象 | 不移动 | 移动 |
| 内存碎片 | 有 | 无 |
| GC 停顿时间 | 较短 | 较长 |
| 代表收集器 | CMS | Serial Old、Parallel Old |
CMS 选择标记-清除是为了追求低延迟,用碎片问题换响应时间。但碎片积攒到一定程度还是得做一次 Compact,这就是 CMS 的 Concurrent Mode Failure 触发 Full GC 的原因之一,代价很大。
四、G1 和 ZGC 的思路
到了 JDK 9 之后,G1 成为默认收集器,它的思路变了——不再严格区分新生代和老年代的物理边界,而是把堆分成一个个等大的 Region(默认约 2048 个)。
- G1 对新生代 Region 用复制算法,对老年代 Region 用标记-整理
- ZGC 更激进,用染色指针和读屏障实现了几乎无碎片的整理,停顿时间控制在亚毫秒级
这块展开就多了,面试官如果追问,说明对你的回答很满意,想看看你的深度。
面试高频追问
-
为什么新生代不用标记-清除?
因为新生代对象存活率极低(通常不到 2%),用复制算法只需要复制少量存活对象,效率极高。如果用标记-清除,每次要扫描整个新生代标记大量垃圾,反而更慢。
-
CMS 为什么选标记-清除而不是标记-整理?
CMS 的设计目标是低延迟。标记-整理需要移动老年代中的大量对象并更新引用,这会造成很长的 STW 停顿,违背了 CMS 的设计初衷。所以 CMS 选择不移动对象的标记-清除,代价是内存碎片。
-
什么是 STW?为什么不可避免?
Stop-The-World,GC 时暂停所有用户线程。因为 GC 需要确保一致性快照(类似数据库的快照隔离),如果一边回收一边有线程修改对象引用,就会出问题。CMS 和 G1 通过并发标记来尽量缩短 STW 时间,但完全消除目前还做不到。
常见面试变体
- "说说 JVM 有哪些垃圾收集算法?各自的特点?"
- "为什么 HotSpot 要采用分代收集?"
- "CMS 和 G1 用的分别是什么算法?"
- "复制算法的缺点是什么?"
记忆口诀
新生代复制、老年代标记——新生代对象死得快,复制少量活的就行;老年代对象活得久,标记完要么清要么整。碎片换速度选清除,稳定无碎选整理。
总结
JVM 的分代 GC 算法选择,本质上是一个 "对症下药" 的过程:新生代对象存活率低,用复制算法效率最高;老年代存活率高,复制算法代价太大,所以选择标记-清除或标记-整理,两者在"碎片"和"延迟"之间做取舍。面试的时候,把"为什么这么分"讲清楚,比单纯背算法名字加分多了。
