云计算百科
云计算领域专业知识百科平台

Java虚拟机:垃圾回收算法

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

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

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

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


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

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

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

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

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

  • 致命缺点:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • 优点:

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

      • Survivor 0 (From):占 10%。

      • Survivor 1 (To):占 10%。

    • 回收过程(Minor GC):

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

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

    • 清理完 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 收集器
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java虚拟机:垃圾回收算法
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!