JVM 垃圾回收算法的详细介绍

我用通俗易懂的方式介绍一下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 的高效性,又能解决内存碎片问题。

相关推荐
AndrewHZ2 小时前
【图像处理基石】什么是光流法?
图像处理·算法·计算机视觉·目标跟踪·cv·光流法·行为识别
mjhcsp3 小时前
C++ 三分查找:在单调与凸函数中高效定位极值的算法
开发语言·c++·算法
立志成为大牛的小牛3 小时前
数据结构——四十二、二叉排序树(王道408)
数据结构·笔记·程序人生·考研·算法
Funny_AI_LAB5 小时前
李飞飞联合杨立昆发表最新论文:超感知AI模型从视频中“看懂”并“预见”三维世界
人工智能·算法·语言模型·音视频
RTC老炮8 小时前
webrtc降噪-PriorSignalModelEstimator类源码分析与算法原理
算法·webrtc
草莓火锅10 小时前
用c++使输入的数字各个位上数字反转得到一个新数
开发语言·c++·算法
散峰而望10 小时前
C/C++输入输出初级(一) (算法竞赛)
c语言·开发语言·c++·算法·github
Kuo-Teng10 小时前
LeetCode 160: Intersection of Two Linked Lists
java·算法·leetcode·职场和发展
fie888910 小时前
基于MATLAB的狼群算法实现
开发语言·算法·matlab