我用通俗易懂的方式介绍一下GC算法,包括标记-清除、复制、标记-整理、分代收集
把 Java 堆想象成一个巨大的 "停车场",里面停满了各种 "车辆"(也就是对象)。有些车是 "正在使用的"(可达对象),有些车是 "废弃的"(垃圾对象)。GC 算法就是不同的 "清障车" 工作模式,它们的目标都是:高效地把废弃的车辆清理掉,腾出空间给新的车辆停放。
目录
- [一、算法一:标记 - 清除(Mark-Sweep)](#一、算法一:标记 - 清除(Mark-Sweep))
- 二、算法二:复制(Copying)
- [三、算法三:标记 - 整理(Mark-Compact)](#三、算法三:标记 - 整理(Mark-Compact))
- [四、算法四:分代收集(Generational Collection)](#四、算法四:分代收集(Generational Collection))
一、算法一:标记 - 清除(Mark-Sweep)
比喻:按名单找车,然后直接拖走。
这是最基础、最直接的一种算法,分为两步:
标记(Mark):清障队拿着一份 "活跃车辆名单"(从 GC Roots 开始遍历),在停车场里找到所有 "正在使用的车",并在它们的挡风玻璃上贴一个 "保留" 的标签。
清除(Sweep):清障队再次遍历整个停车场,把所有没有"保留" 标签的车(废弃车辆)全部拖走。清理完后,他们会记录下这些空出来的 "停车位"(内存碎片),方便以后分配。
核心思想:分两步:① 标记:遍历所有对象,标记存活对象;② 清除:遍历堆内存,回收未标记的垃圾对象,释放内存。
优点:
简单直接:实现容易,思路清晰。
不需要移动对象:只做标记和清除,不改变对象的位置。
缺点:
效率不稳定:需两次全堆遍历,两次遍历(标记和清除)都会很慢。
产生内存碎片:回收后内存空间不连续,大对象可能无法分配。
二、算法二:复制(Copying)
比喻:把有用的车挪到新停车场,旧停车场直接废弃。
为了解决标记 - 清除的碎片问题,复制算法应运而生。它把停车场一分为二,通常是 From 区 和 To 区。
复制(Copy):清障队拿着 "活跃车辆名单",把 From 区里所有 "正在使用的车" 都完好无损地复制到 To 区,并且是紧凑地、连续地停放。
交换(Swap):复制完成后,From 区里剩下的肯定全是废弃车辆。这时,清障队直接宣布整个 From 区作废,然后把 From 区和 To 区的角色互换。下一次 GC 时,就从新的 From 区复制到新的 To 区。
核心思想:将内存分为大小相等的两块(如 From 区和 To 区),仅使用其中一块。回收时,将存活对象复制到另一块区域,然后清空原区域。
优点:
效率高:只需复制存活对象,无需遍历垃圾。
无内存碎片:新对象总是在一个干净、连续的空间里分配,像新建的停车场一样整齐。
缺点:
空间浪费:内存利用率低(仅用一半内存)。
对象移动成本高:存活对象多时,复制成本高。如果 "正在使用的车" 很多,复制它们会非常耗时,并且需要更新所有指向这些被移动对象的 "引用"(相当于通知所有车主他们的车位变了)。
三、算法三:标记 - 整理(Mark-Compact)
比喻:先标记要保留的车,然后把它们都挪到停车场的一端,剩下的一次性清空。
这是为了解决 "标记 - 清除" 的碎片问题和 "复制" 算法的空间浪费问题而设计的,主要用于老年代。
标记(Mark):和 "标记 - 清除" 算法一样,先给所有 "正在使用的车" 贴上 "保留" 标签。
整理(Compact):清障队把所有贴了 "保留" 标签的车,像 "拼图" 一样,向停车场的一端移动,让它们紧紧地挨在一起。
清除(Clear):所有保留的车都移走后,停车场另一端剩下的一大片连续区域就可以一次性全部清空。
核心思想分两步:① 标记:同标记 - 清除,标记存活对象;② 整理:将存活对象向内存一端移动,然后清除边界外的所有垃圾。
优点:
无内存碎片:整理后,内存空间是连续的。
内存利用率高:不需要像复制算法那样预留一半空间 。
缺点:
效率更低:在标记的基础上,增加了 "移动对象" 的步骤,这个过程非常耗时,尤其是对象很多的时候。
四、算法四:分代收集(Generational Collection)
比喻:按车辆使用频率,分不同区域管理。
这不是一种具体的算法,而是一种 "分而治之" 的策略 。它基于一个重要的观察:大部分对象都是 "朝生夕灭" 的(新生代),只有少数对象能活很久(老年代)。于是,停车场被划分为两个区域:
新生代(Young Generation):
比喻:"临时停车区"。 特点:车流量大,大部分车停一会儿就走。
算法:采用复制算法。因为新生代中大部分都是垃圾,需要复制的存活对象很少,效率极高。它内部又分为一个 Eden 区和两个 Survivor
区(From 和 To)。
老年代(Old Generation):
比喻:"长期停车区"。 特点:车很稳定,进来了就可能停很久。
算法:采用标记 - 清除或标记 - 整理算法。因为老年代中大部分对象都是存活的,使用复制算法成本太高。
工作流程:
1.新车(新对象)先停在新生代的 Eden 区。
2.Eden 区满了,触发一次 Minor GC(新生代 GC)。 把 Eden 和 From Survivor 区里存活的对象,复制到 To Survivor 区。 清空 Eden 和 From Survivor 区。 From 和 To 区角色互换。
3.对象在 Survivor 区之间来回被复制,每复制一次 "年龄" 就加一岁。 当对象年龄达到一个阈值(比如 15 岁),就会被 "晋升" 到老年代。 当老年代也快满了,就会触发 Major GC / Full GC(老年代GC),这个过程通常比较慢。
核心思想:根据对象存活周期将内存分为多代(如年轻代、老年代),不同代采用不同算法:
年轻代(对象存活时间短,垃圾多):用复制算法(高效处理大量短期对象)。
老年代(对象存活时间长,垃圾少):用标记 - 清除或标记 - 整理算法(减少对象移动成本)。
优点:
极高的整体 GC 效率:针对不同生命周期的对象,使用最合适的算法,扬长避短。新生代 GC 非常快,而老年代 GC
虽然慢,但发生频率低。结合不同算法的优势,兼顾效率和内存利用率,是目前 JVM 主流 GC 策略(如 G1、CMS)。
缺点:
实现复杂,需维护多代内存管理逻辑。
总结对比
| 算法 | 比喻 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 按名单拖走废车 | 实现简单,不移动对象 | 效率低,产生碎片 | 老年代(作为兜底或与整理结合) |
| 复制 | 挪走有用的车,废弃旧车场 | 效率高,无碎片 | 空间浪费,移动成本高 | 新生代 |
| 标记 - 整理 | 挪车挤到一端,再清另一端 | 无碎片,内存利用率高 | 效率低,移动成本高 | 老年代 |
| 分代收集 | 分临时 / 长期停车区管理 | 综合效率最高,应用最广 | 实现复杂 | 所有现代 JVM 的标准策略 |
一句话总结:现代 JVM 都采用分代收集策略,在新生代用复制算法,在老年代用标记 - 清除 / 整理算法,这样既能保证 GC 的高效性,又能解决内存碎片问题。