JVM虚拟机:垃圾收集算法

这段内容主要讲解了**追踪式垃圾收集(Tracing GC)的核心理论基础以及三种主要算法的演进过程。**

1. 理论基石:分代收集理论 (Generational Collection)

现代商业虚拟机(如HotSpot)之所以高效,是因为它们不是"一视同仁"地回收内存,而是根据对象存活的时间长短,将堆内存划分为新生代(Young Gen)老年代(Old Gen)

这种设计建立在三条假说之上:

  • 弱分代假说: 绝大多数对象都是"朝生夕灭"的(一出生很快就变成垃圾)。

  • 强分代假说: 熬过越多次GC的对象,就越难以消亡(活得越久,越不容易死)。

  • 跨代引用假说: 跨代引用(老年代引用新生代)相对于同代引用仅占极少数。

设计推论:

基于前两条假说,新生代适合高频回收(关注"死"的),老年代适合低频回收(关注"活"的)。基于第三条假说,为了解决跨代扫描问题,引入了记忆集(Remembered Set),将老年代切块,只标记有跨代引用的块,从而避免在回收新生代时扫描整个老年代。


2. 三大核心算法演进

垃圾收集算法的发展本质上是对内存空间利用率执行效率(停顿时间)的权衡。

2.1 标记-清除算法 (Mark-Sweep)

这是最基础的算法,后续算法多是基于此改进。

  • 原理:

    1. 标记: 找出所有需要回收(或存活)的对象。

    2. 清除: 统一回收被标记的对象。

  • 缺点:

    • 效率不稳定: 垃圾越多,标记和清除的操作越慢。

    • 内存碎片化: 清除后会产生大量不连续的内存碎片,导致无法为大对象分配空间,从而提前触发下一次GC。

2.2 标记-复制算法 (Mark-Copy)

为了解决"效率"和"碎片"问题而生,主要用于新生代

  • 原理(半区复制): 将内存分为两块,每次只用一块。满时将存活对象复制到另一块,清空当前块。

  • 优化(Appel式回收):

    • 由于新生代98%的对象都会死,不需要1:1划分。

    • HotSpot布局: 1个 Eden 区 + 2个 Survivor 区(比例 8:1:1)。

    • 利用率: 每次使用 Eden + 1个 Survivor(90%空间),只浪费10%。

    • 逃生门: 如果存活对象超过10%,通过分配担保机制直接进入老年代。

  • 优点: 运行高效,无内存碎片。

  • 缺点: 需要浪费一部分空间(原版浪费50%,优化版浪费10%)。

2.3 标记-整理算法 (Mark-Compact)

针对老年代对象存活率高的特点设计。

  • 原理: 标记过程同Mark-Sweep,但后续不是直接清除,而是让所有存活对象向内存一端移动,然后清理边界外的内存。

  • 权衡(Trade-off):

    • 移动对象: 会导致应用程序暂停(Stop The World),增加延迟,但内存规整,后续分配和访问快(高吞吐量)。

    • 不移动对象: GC停顿短,但内存碎片化严重,分配慢。

  • 应用: 关注吞吐量的收集器(如Parallel Scavenge)使用此算法;关注延迟的(如CMS)平时用标记-清除,碎片严重时才用标记-整理。


3. 算法对比总结

特性 标记-清除 (Mark-Sweep) 标记-复制 (Mark-Copy) 标记-整理 (Mark-Compact)
适用区域 老年代 (如CMS) 新生代 (主流) 老年代 (主流)
空间开销 无 (但有碎片) 高 (需预留Swap区)
移动对象
内存碎片 严重
主要优点 实现简单,无需移动对象 效率极高,无碎片 无碎片,内存规整
主要缺点 碎片化导致频繁GC 空间利用率受限 (浪费10%-50%) 移动对象需暂停应用 (STW)

4. 关键术语定义 (防止混淆)

文中特别强调了不同GC类型的定义,这在阅读GC日志时非常重要:

  • Minor GC / Young GC: 只回收新生代(最常见)。

  • Major GC / Old GC: 只回收老年代(目前只有CMS收集器有单独的Old GC行为)。注意:有时Major GC也被指代整堆收集,需看上下文。

  • Mixed GC: 回收整个新生代 + 部分老年代(G1收集器特有)。

  • Full GC: 回收整个Java堆和方法区(代价最大,应尽量避免)。

问答


第一部分:分代收集理论基础

Q1:当前主流商用虚拟机的垃圾收集器大多遵循什么理论进行设计?这个理论的基础是什么?

A: 大多遵循"分代收集"(Generational Collection)理论。

它的基础是两个分代假说(经验法则):

  1. 弱分代假说:绝大多数对象都是朝生夕灭的(生命周期很短)。

  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

  3. 引入跨代引用假说:跨代引用相对于同代引用仅占极少数,据此建立记忆集(Remembered Set)。

Q2:基于分代假说,垃圾收集器的一致设计原则是什么?这样做有什么好处?

A: 设计原则是:应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(熬过GC的次数)分配到不同的区域之中存储。通常分为新生代(Young Generation)和老年代(Old Generation)。

好处:

  • 针对新生代:如果一个区域大多数对象都是朝生夕灭的,把它们集中在一起,每次回收只关注如何保留少量存活对象,能以极低代价回收大量空间。

  • 针对老年代:如果剩下的都是难以消亡的对象,把它们集中在一起,可以使用较低的频率来回收。

  • 总体:同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

Q3:分代收集理论面临的一个明显困难是什么?如何解决?

A: 困难是跨代引用。对象不是孤立的,老年代对象可能引用新生代对象。在进行Minor GC(只回收新生代)时,为了确保可达性分析正确,理论上需要遍历整个老年代来查找对新生代的引用,这会带来巨大的性能负担。

解决方案:

引入跨代引用假说(跨代引用相对于同代引用仅占极少数),并据此建立记忆集(Remembered Set)。

记忆集在新生代中建立,它把老年代划分成小块,只标识出哪一块内存存在跨代引用。发生Minor GC时,只需扫描记忆集标识出的包含跨代引用的小块内存,而无需扫描整个老年代。


第二部分:三大核心垃圾收集算法

Q4:请简述"标记-清除"(Mark-Sweep)算法的过程及其主要缺点。

A:

过程:分为"标记"和"清除"两个阶段。首先标记出所有需要回收的对象(或存活对象),标记完成后,统一回收掉所有被标记的对象(或未被标记的对象)。

主要缺点:

  1. 执行效率不稳定:如果堆中包含大量对象且大部分需要回收,必须进行大量标记和清除动作,效率随对象数量增长而降低。

  2. 内存空间的碎片化问题:清除后会产生大量不连续的内存碎片。碎片太多会导致以后需要分配大对象时,无法找到足够连续内存而提前触发下一次GC。

Q5:"标记-复制"(Mark-Copy)算法主要是为了解决什么问题而提出的?它非常适合应用于哪个年代?

A: 它主要是为了解决标记-清除算法在面对大量可回收对象时执行效率低的问题(以及碎片问题)。

它非常适合应用于新生代,因为新生代对象具有"朝生夕灭"的特点,存活率低,复制成本小。

Q6:请描述原始的"半区复制"算法以及HotSpot虚拟机中优化的"Appel式回收"策略。

A:

  • 原始半区复制:将可用内存按容量划分为大小相等的两块,每次只使用一块。当这一块用完,就将还存活的对象复制到另一块上,然后清理掉已使用过的那一块。缺点是内存利用率只有50%。

  • Appel式回收(HotSpot优化):鉴于新生代98%对象熬不过第一轮收集,不需要1:1划分。

    • 做法:把新生代分为一块较大的Eden空间和两块较小的Survivor空间(HotSpot默认比例8:1:1)。

    • 过程:每次分配只使用Eden和其中一块Survivor。发生GC时,将Eden和该Survivor中存活的对象一次性复制到另一块Survivor空间,然后清理掉Eden和已用过的那块Survivor。

    • 空间利用率:每次新生代可用空间为90%(80% Eden + 10% Survivor),只有10%被"浪费"。

Q7:在Appel式回收中,如果Survivor空间不足以容纳一次Minor GC后存活的对象怎么办?

A: 需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。这些存活对象将通过分配担保机制直接进入老年代。

Q8:为什么老年代不能直接使用"标记-复制"算法?"标记-整理"(Mark-Compact)算法是如何工作的?

A:

原因:老年代对象存活率较高,使用复制算法会进行较多的复制操作,效率降低。更关键的是,如果不想浪费50%空间,就需要额外的空间进行分配担保,以应对所有对象都100%存活的极端情况,而老年代一般没有其他区域为其担保。

标记-整理算法工作过程:

标记过程与"标记-清除"一样,但后续步骤不是直接清理可回收对象,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

Q9:对比"标记-清除"和"标记-整理",它们本质差异是什么?移动存活对象有什么优缺点?

A:

本质差异:前者是非移动式的回收算法,后者是移动式的。

移动存活对象的优缺点(权衡):

  • 缺点(风险):在老年代这种大量对象存活的区域移动对象并更新引用,是一种极为负重的操作,且必须全程暂停用户应用程序(Stop The World),增加延迟。

  • 优点(收益):如果不移动对象,内存碎片问题只能依赖复杂的内存分配器(如分区空闲分配链表)来解决,这会增加内存访问的负担,进而影响应用程序的吞吐量。移动对象虽然回收时更复杂,但能获得规整的内存空间,使内存分配和访问更高效,从整体吞吐量来看是划算的。


相关推荐
数电发票API2 小时前
线上充值自动开票攻略:四步落地,告别人工低效内耗
java
练习时长一年2 小时前
LeetCode热题100(分割等和子集)
算法·leetcode·职场和发展
想用offer打牌2 小时前
Spring AI vs Spring AI Alibaba
java·人工智能·后端·spring·系统架构
七号驿栈2 小时前
07_汽车信息安全算法在线验证工具(测试报告)
算法
顾北122 小时前
Java接入阿里百炼大模型实战指南
java·ai
毕设源码-郭学长2 小时前
【开题答辩全过程】以 高校水电表缴费系统的设计与实现为例,包含答辩的问题和答案
java
win x2 小时前
网络通信协议 第一部
java·网络协议
啦哈拉哈2 小时前
【Python】知识点零碎学习4
python·学习·算法
爱喝可乐的老王2 小时前
线性回归模型案例:广告投放效果预测
算法·回归·线性回归