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