jvm垃圾回收之复制算法——为什么分两块Survivor空间

本文深入探讨了Java虚拟机中复制垃圾收集算法的工作原理,特别是对于新生代中Eden和Survivor空间的具体分配和使用策略。解释了为何需要两个Survivor空间而非一个,并对比分析了两种方案的技术实现复杂度。

复制算法的两块Survivor空间

概述

在《深入理解Java虚拟机》这本书中,对复制算法有一段这样的介绍:现在的商业虚拟机大多采用复制算法来收集新生代。复制算法将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间,当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor上,最后清理掉Eden和刚才使用的空间。对虚拟机不是很熟悉的同学,应该会有这样的疑问:为什么硬要分成两块Survivor空间呢,明明其中一块跟Eden的功能是一样的。

 

复制算法原理  

Survivor区,一块叫From,一块叫To,对象存在Eden和From块。当进行GC时,Eden存活的对象全移到To块,而From中,存活的对象按年龄值确定去向,当达到一定值(年龄阈值,通过-XX:MaxTenuringThreshold可设置)的对象会移到年老代中,没有达到值的复制到To区,经过GC后,Eden和From被清空。

  之后,From和To交换角色,新的From即为原来的To块,新的To块即为原来的From块,且新的To块中对象年龄加1.

 

疑问

只分一块Survivor区,当进行GC时,先将Survivor区中存活的对象达到年龄值的移入年老代,清除已死亡的对象,后将Eden区中存活的对象移入Survivor区,将Survivor区的对象年龄都加1.这样与分两块Survivor有什么区别呢,为什么虚拟机一定要分成两块呢?

 

解答

在查阅百度及和小伙伴讨论后,得出一个结论,有不妥之处或有更好的想法,欢迎指正。

即若只分一块Survivor,在清除Survivor区已死亡的对象时,因为此刻的Survivor区还有存活的对象,清除要比分两块Survivor麻烦,两块的情况,回收时只需将存活的对象移走,剩下的对象直接清理即可。

另外,分成两块Survivor,From和To分工明确,逻辑理解和技术实现较简单。

 

JVM垃圾回收(Garbage Collection, GC)是自动内存管理的核心机制,其目标是**识别并回收不再使用的对象**,释放内存空间。为了实现这一目标,JVM 使用了多种 **垃圾回收算法**,这些算法在不同的垃圾收集器中被组合使用。 --- ## ✅ 一、JVM 主要的垃圾回收算法 以下是 JVM 中常用的几种核心垃圾回收算法: | 算法 | 英文名称 | 特点 | |------|----------|------| | 1. 标记-清除 | Mark-Sweep | 最基础的算法“标记”和“清除”两个阶段 | | 2. 标记-复制 | Mark-Copy / Copying | 将存活对象复制到另一块区域,适合新生代 | | 3. 标记-整理(或标记-压缩) | Mark-Compact | 存活对象向一端滑动,消除内存碎片 | | 4. 代收集 | Generational Collection | 基于对象生命周期划区域,不同代用不同算法 | | 5. 增量收集 | Incremental Collection | 小步执行 GC,减少 STW 时间(较少用) | | 6. 并发标记(CMS/G1 使用) | Concurrent Mark-Sweep | 部阶段与应用线程并发执行 | 我们来逐个详细解释。 --- ### 1. 🟢 标记-清除(Mark-Sweep) #### 原理: - **第一阶段:标记(Mark)** - 从 GC Roots 出发,遍历所有可达对象,做上“存活”标记。 - **第二阶段:清除(Sweep)** - 遍历堆,回收未被标记的对象内存。 #### 示例代码逻辑(伪代码): ```java // GC Roots: static variables, local variables, etc. Set<Object> liveObjects = markFromGCRoots(); for (Object obj : heap) { if (!liveObjects.contains(obj)) { free(obj); } } ``` #### ✅ 优点: - 实现简 - 不移动对象 #### ❌ 缺点: - **产生内存碎片** → 可能导致大对象无法配 - 清除过程需要遍历整个堆,效率不高 #### 应用场景: - CMS 收集器的老年代回收使用此算法(但会配合压缩防止碎片化) --- ### 2. 🔵 标记-复制(Mark-Copy 或 Copying) #### 原理: - 将内存两块:**From 空间 和 To 空间** - 回收时,把 From 中所有存活对象复制到 To - 然后一次性清空 From 空间 #### 流程: 1. 存活对象从 Eden + Survivor A 复制Survivor B 2. 清空 EdenSurvivor A 3. 下次回收时角色互换 #### ✅ 优点: - 没有内存碎片 - 复制后内存连续,配快(指针碰撞) - 适合“朝生夕死”的对象(如新生代) #### ❌ 缺点: - 内存利用率只有 50%(如果不优化) - 如果存活对象多,复制开销大 #### 优化方案: - 使用 **Eden : S0 : S1 = 8:1:1** 结构,提高利用率 - 只复制存活对象,死亡对象直接丢弃 #### 应用场景: - 所有新生代收集器(如 Parallel Scavenge、G1GC 新生代阶段)都使用该策略 --- ### 3. 🟡 标记-整理(Mark-Compact) #### 原理: - **第一阶段:标记** —— 同 Mark-Sweep - **第二阶段:整理(Compact)** —— 将所有存活对象向内存的一端移动 - **第三阶段:清理边界外内存** #### 示例图示: ``` 原始:[A][B][ ][C][ ][ ][D][ ] 整理后:[A][B][C][D] ← 剩余空间连续 ``` #### ✅ 优点: - 消除内存碎片 - 提高内存利用率 - 适合长期存活对象 #### ❌ 缺点: - 移动对象需要更新引用指针,开销较大 - 整体耗时较长 #### 应用场景: - 老年代的主要回收方式 - 如 Parallel Old GC、CMS 的后备 Full GC、G1GC 的 Full GC 等 --- ### 4. 🟣 代收集(Generational Collection) 这不是一种具体的算法,而是一种 **设计思想**,基于以下观察: > 绝大多数对象都是“朝生夕灭”,少数对象会长期存活。 #### JVM 内存代结构: ``` 堆(Heap) ├── 新生代(Young Generation) │ ├── Eden 区 │ ├── Survivor 0 │ └── Survivor 1 └── 老年代(Old Generation) ``` #### 不同代使用不同算法: | 代 | 使用算法 | 原因 | |----|--------|------| | 新生代 | **标记-复制(Copying)** | 对象死亡率高,复制成本低 | | 老年代 | **标记-清除 或 标记-整理** | 存活对象多,不适合复制 | #### 触发条件: - Minor GC:Eden 区满时触发(频繁) - Major GC / Full GC:老年代满或元空间不足时触发(少见但影响大) #### 应用: - 所有现代 JVM 收集器(Parallel、CMS、G1、ZGC)均采用代设计(ZGC/Shenandoah 除外,它们弱化了代) --- ### 5. ⚪ 增量收集(Incremental GC) #### 原理: - 把一次大的 GC 拆成多个小步骤 - 每次只回收一部区域,穿插运行用户线程 #### 目标: - 减少次 STW(Stop-The-World)时间 #### 缺点: - 总体 GC 时间变长 - 容易出现“浮动垃圾” - 实现复杂,效果有限 #### 现状: - 已基本被淘汰 - 现代 GC 更倾向于“并发”而非“增量” --- ### 6. 🔴 并发标记(Concurrent Marking) 这是 CMS 和 G1 等低延迟 GC 的核心技术之一。 #### 原理: - 标记阶段尽可能与应用线程**并发执行** - 只在关键点短暂暂停(如初始标记、重新标记) #### 步骤(以 CMS 为例): 1. 初始标记(STW)—— 快速标记 GC Roots 直接引用 2. 并发标记 —— 和应用一起跑,遍历对象图 3. 重新标记(STW)—— 修正并发期间的变化 4. 并发清除 —— 回收无引用对象 #### ✅ 优点: - 大幅降低停顿时间 - 适合响应敏感系统(如 Web 服务) #### ❌ 缺点: - 占用 CPU 资源 - 可能发生“并发模式失败”(Concurrent Mode Failure)→ 退化为 Full GC - 仍有 STW 阶段 #### 应用场景: - CMS GC(Java 8 可用,已废弃) - G1GC 的并发标记周期 - ZGC / Shenandoah 的全程并发标记 --- ## ✅ 二、主流垃圾收集器使用的算法组合 | 收集器 | 新生代算法 | 老年代算法 | 特点 | |--------|-------------|-------------|------| | **Serial GC** | Mark-Copy | Mark-Compact | 线程,用于客户端模式 | | **Parallel GC** | Mark-Copy | Mark-Compact | 多线程,高吞吐 | | **CMS GC** | Mark-Copy | Mark-Sweep(+可选压缩) | 低延迟,但易碎片化 | | **G1GC** | Region-based Copy | Evacuation(类似复制) | 可预测停顿,适合大堆 | | **ZGC** | 并发标记 + 并发重定位 | 全程并发 | <10ms 停顿,JDK 11+ | | **Shenandoah** | 并发压缩 | 并发整理 | JDK 12+,独立于 Oracle | --- ## ✅ 三、如何选择合适的 GC 算法? | 应用类型 | 推荐算法/收集器 | 理由 | |---------|------------------|------| | 批处理任务 | Parallel GC | 吞吐优先,充利用 CPU | | Web 微服务 | G1GC 或 ZGC | 低延迟,避免卡顿 | | 大数据处理 | G1GC | 大堆友好,可控停顿 | | 极致低延迟 | ZGC / Shenandoah | 几乎无 STW | | 嵌入式设备 | Serial GC | 资源受限,简可靠 | --- ## ✅ 四、总结对比表 | 算法 | 是否移动对象 | 是否有碎片 | 适用场景 | 典型收集器 | |------|---------------|------------|-----------|-------------| | Mark-Sweep | 否 | 是 | 快速清除,容忍碎片 | CMS | | Mark-Copy | 是 | 否 | 新生代,存活少 | Parallel, G1 | | Mark-Compact | 是 | 否 | 老年代整理 | Parallel Old | | Generational | 组合使用 | —— | 通用 | 所有主流 GC | | Concurrent Mark | 部并发 | 控制碎片 | 低延迟 | CMS, G1, ZGC | --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值