Java虚拟机:垃圾回收算法
2026/7/25 18:07:51 网站建设 项目流程

一、 垃圾回收的核心思想:可达性分析

垃圾回收(GC,Garbage Collection)到底是在回收什么?它的基本思想可以归纳为一句话:考察每一个对象的“可触及性”

  • 如何判断“活”的?JVM设定了一些“根对象(GC Roots)”(如栈帧中的本地变量、静态变量等)。从这些根节点出发,只要能够顺着引用链一路追踪到的对象,就说明它正在被使用,是活着的。

  • 如何判断“死”的?如果从所有的根节点出发,都无法访问到某个对象,说明这个对象已经“与世隔绝”,可以被当作垃圾回收了。


二、 四大核心垃圾回收算法演进

虚拟机并没有使用单一算法,而是根据不同场景采用了不同策略。我们来看看它们各自的优劣。

1. 引用计数法(被淘汰的“古老方法”)

这是最简单粗暴的方法:每个对象配一个计数器,有人引用就+1,引用失效就-1。计数器为0,就回收。

  • 优点:实现极其简单,实时性好。

  • 致命缺点

    1. 无法解决“循环引用”(这是它被Java抛弃的根本原因)。比如对象A引用了B,B引用了A,但除此之外没别人引用它们了。此时A和B的计数器都是1,它们都认为对方还活着,导致垃圾无法被回收,造成内存泄漏

    2. 性能消耗:每次赋值都要加减计数器,对系统性能有损耗。

结论:Java 虚拟机从未采用引用计数法。

2. 标记清除法(Mark-Sweep):现代算法的基础

这是最基础的算法,分为两个阶段:

  1. 标记:通过根节点,把能访问到的存活对象统统打上标记。

  2. 清除:把没有标记的对象直接抹掉,释放空间。

  • 优点:不需要额外的空间,实现起来相对直接。

  • 致命缺点:内存碎片化
    回收后的内存空间是不连续的。就像一块巨大的奶酪被挖得千疮百孔。如果你想要分配一个大对象(比如一个大数组),因为找不到连续的空间,明明总空间够,却分配失败,触发后续的垃圾回收,严重降低效率。

3. 复制算法(Copying):新生代的“救星”

既然标记清除会有碎片,那我们把内存一分为二,每次只用一半,如何?

  1. 划分:把内存分成大小相等的两块 A 和 B。只使用 A。

  2. 回收时:把 A 中存活的对象,一口气全部复制到 B 中。然后直接把 A 全部清空。

  3. 交换:交换 A 和 B 的角色,下次只使用 B。

  • 优点

    • 绝对无碎片:对象是紧密复制到新空间的,排列整齐。

    • 效率极高:特别适合“垃圾多、存活对象少”的场景。需要拷贝的只有那么几个幸存者。

  • 致命缺点内存利用率低,直接打折。只能使用总内存的一半,另一半总是闲置,对内存开销太大。

👉 小贴士:因为在新生代中,90%的对象都会“朝生夕灭”(活不过第一次回收),所以复制算法在新生代大放异彩。

4. 标记压缩法(Mark-Compact):老年代的“终极武器”

那如果在老年代(对象存活率高),用复制算法岂不是要把大量的对象搬来搬去,成本极高?于是,标记压缩法应运而生,它也被称为Mark-Sweep-Compact(标记-清除-压缩)

  1. 标记:和标记清除一样,先找出所有存活对象。

  2. 压缩(关键):不直接清除垃圾。而是将所有的存活对象往内存的一端移动,挤在一起。

  3. 清除:清理掉边界以外的所有内存空间。

  • 优点:既解决了碎片化问题,又不需要像复制算法那样浪费一半的内存。

  • 缺点移动对象需要暂停应用线程(Stop-The-World),如果存活对象太多,移动成本较高。


三、 实战组合:分代算法(Generational Collection)

既然每种算法都有优缺点,聪明的JVM设计者提出了“分代假设”“绝大多数对象都是朝生夕死的;熬过多次回收的对象,则很难死去。”
基于这个假设,JVM将堆内存划分为新生代老年代,分别使用不同的算法。

📍 新生代(Young Generation) - 使用【复制算法】

  • 特点:对象存活率极低。

  • 内存分配:并不是简单的 1:1 分割。为了优化空间利用率,JVM 将新生代划分为:

    • Eden(伊甸园):占 80%。新对象诞生地。

    • Survivor 0 (From):占 10%。

    • Survivor 1 (To):占 10%。

  • 回收过程(Minor GC)

    1. 回收时,Eden 和 From 区中的存活对象,会一股脑复制到To区。

    2. 如果对象特别大,或者 To 区满了,就直接晋升到老年代

    3. 清理完 Eden 和 From 后,交换From 和 To 的角色。

  • 💡 为什么这样设计?这样既保留了复制算法“无碎片、效率高”的优点,又将空间浪费从50%降低到了10%(只浪费一个Survivor区)。

📍 老年代(Old Generation) - 使用【标记压缩法】

  • 特点:存活率高,生命周期长。

  • 回收过程(Major GC / Full GC):使用标记压缩法,效率更高,但同时产生的停顿时间也明显长于新生代回收。

📍 冷知识:卡表(Card Table) - 解决跨代引用性能瓶颈

老年代的对象有可能引用新生代的对象。如果在新生代回收时,要遍历整个老年代去检查有没有引用,那速度慢得不可想象。
JVM 使用了一个巧妙的位图数据结构——卡表

  • 工作原理:将老年代划分成许多 4KB 的小块。卡表中的一个 bit 就代表这块 4KB 区域。

    • 0:表示这个 4KB 区域内的老年代对象,没有任何一个引用了新生代对象。

    • 1:表示这个 4KB 区域内,至少有一个对象引用了新生代对象。

  • 效果:在新生代 GC 时,只需查看卡表。如果卡表位是 0,直接跳过该区域。这大大减少了扫描对象的时间,加速了垃圾回收。


四、 现代趋势:分区算法(G1收集器的核心)

说完分代,我们再来看一种近年来非常火热的思想——分区算法(主要应用于 G1 垃圾收集器)。

  • 痛点:分代回收中,一旦触发 Full GC(针对整个老年代/堆),如果内存巨大,停顿时间(STW)会非常久,严重影响业务。

  • 解决办法:放弃物理上的新生代/老年代划分,把整个堆空间划分成若干大小相等的“小区间”(Region)。每一个小区间都可以独立使用、独立回收。

  • 优势:由于区间很小,GC 时不需要回收整个堆,而是根据目标停顿时间,每次回收一部分(比如 20 个)区间。这就把原来几秒钟的大停顿,拆解成了几十毫秒的小停顿,避免了“全屋大扫除”带来的业务阻塞。


五、 技术总结与面试避坑指南

算法名称核心操作优点缺点适用区域
标记清除标记+清除垃圾简单、空间利用率高产生内存碎片老年代(CMS收集器)
复制算法存活对象复制到新空间无碎片、速度快空间浪费严重(需双倍空间)新生代(Eden/Survivor)
标记压缩标记+压缩存活对象无碎片、空间利用率高移动对象耗时,停顿时间长老年代
分代算法结合各算法优点针对性强,整体效率最佳需处理跨代引用(卡表)整堆(主流的宏观策略)
分区算法分小块独立回收控制停顿时间精准实现难度高G1/Garbage-First 收集器

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询