欢迎光临
我们一直在努力

别焦虑 AI 抢饭碗了!先看看 JVM 是如何对你代码里的垃圾对象进行“物理超度”的

在 Java 后端开发中,JVM 的自动内存管理机制让我们彻底告别了 C/C++ 时代手动释放内存的繁琐与危险。但这并不意味着我们可以对内存放任不管——当系统出现卡顿、OOM(内存溢出)等性能瓶颈时,深入理解垃圾回收(GC)机制就成了解决问题的关键。

今天,让我们一步步剥丝抽茧,彻底搞懂 JVM 的垃圾回收机制。

一、如何判断一个对象是“垃圾”?

谈到垃圾回收,我们的第一个问题必然是:内存中这么多对象,谁该留,谁该走?

业界通常有两种算法来判定对象的存活状态:

1. 引用计数法

最直观的想法是给每个对象配一个“计数器”。有其他变量引用它,计数器加 1;引用断开,计数器减 1。计数器归零时,说明没人用它了,直接回收。

  • 致命缺陷: 无法解决循环引用问题。假设对象 A 引用对象 B,对象 B 引用对象 A,但外界已经没有任何变量指向它们。这两个对象实际上已经是垃圾,但因为计数器互相撑着都不为 0,导致永远无法被回收。

2. 可达性分析

为了解决循环引用,Java 采用了可达性分析。 它的核心思想就像是“顺藤摸瓜”:以一系列被称为 GC Roots(根对象)的变量作为起点,向下遍历它们所引用的对象,形成一张有向图。

  • 存活对象: 能够被这条藤蔓关联到的对象。

  • 垃圾对象: 游离在图外,GC Roots 无法到达的对象。


二、遍历的艺术:三色标记法

可达性分析只是一个“思想”,在具体的并发执行过程中,JVM 是如何高效完成对象图遍历的呢?这就引入了三色标记法。

在这个过程中,我们将对象分为三种颜色:

  • 白色:尚未被访问到的对象。如果遍历结束依然是白色,那就是要被清理的垃圾。

  • 灰色:对象本身已被访问,但它内部引用的其他对象还没被全部扫描,属于“待办事项”。

  • 黑色:对象本身及内部引用的对象都已扫描完毕,是绝对安全的存活对象。

标记的流转过程:

  • GC 开始时,所有对象都是白色;GC Roots 直接关联的对象被涂成灰色。

  • 从灰色对象出发,把它引用的白色对象涂成灰色,它自己因为扫描完毕,变成黑色。

  • 重复这个过程,直到场上再也没有灰色对象。最后剩下的白色对象,就是不可达的垃圾。

  • 三色标记面临的挑战

    由于现代 JVM 允许垃圾回收线程和用户线程并发执行,在标记的过程中,用户线程可能会偷偷改变引用关系,导致两个经典问题:

  • 浮动垃圾(Floating Garbage): 某个对象本来被标记成了黑色(判定存活),但用户线程突然断开了对它的引用。它实际上成了垃圾,但本轮 GC 已经不会再管它了,只能等下一次 GC 兜底。这个问题无伤大雅,内存够用就行。

  • 漏标(Missed Marks): 这是致命错误。存活的对象被当成垃圾清了!它必须同时满足两个极端条件:

    • 黑色对象突然新增了对某个白色对象的引用。

    • 所有灰色对象对该白色对象的引用被全部切断。

  • 三、三大垃圾回收算法

    查出了谁是垃圾,接下来就是“怎么回收”的问题。基础的垃圾回收算法主要有三种:

    算法名称 核心思想 优点 缺点 适用场景
    标记-清除 (Mark-Sweep) 先标记存活对象,然后直接把不可达对象的内存抹掉。 实现简单。 会产生大量内存碎片,导致后续大对象无法分配。 存活对象多,垃圾少的区域。
    标记-整理 (Mark-Compact) 标记存活对象,然后让所有存活对象向内存一端移动,直接清理掉边界外的空间。 没有内存碎片。 需要移动对象并更新引用,速度慢,成本高。 存活对象多,垃圾少的区域。
    复制 (Copying) 将内存一分为二,每次只用一半。存活对象统一复制到另一半,然后直接清空当前半区。 效率极高,没有内存碎片。 内存利用率极低,永远有一半空间闲置。 存活对象少,垃圾极多的区域。

    四、分代回收机制

    通过对比发现,没有一种算法是完美的。于是,JVM 引入了分代收集理论——这并非一种新算法,而是一种内存管理策略。

    JVM 将堆内存分为 新生代 (Young Gen) 和 老年代 (Old Gen),根据对象生命周期的长短,分配不同的算法,实现效率最大化。

    1. 新生代(朝生夕死)

    新生代的特点是对象产生快,死得也快(比如局部变量)。这里极其适合复制算法,因为真正存活的对象很少,复制成本极低。

    新生代进一步被划分为一个 Eden 区和两个 Survivor 区(From 和 To)。

    • Minor GC(Young GC)流程: 新对象优先在 Eden 区分配。当 Eden 满了,触发 Minor GC。此时会发生 STW(Stop The World,暂停所有用户线程),回收器将 Eden 和 From 区依然存活的对象复制到 To 区,对象年龄 +1。随后清空 Eden 和 From,并交换 From 和 To 的角色。

    2. 晋升老年代(老谋深算)

    对象不会永远留在新生代,满足以下条件会晋升到老年代:

  • 年龄达标: 在 Survivor 区熬过 15 次 GC(默认阈值)的老寿星。

  • 大对象直通: 超大对象直接进入老年代,避免在新生代频繁来回复制。

  • 动态年龄判定: 如果 Survivor 区中相同年龄的对象总大小超过空间的一半,大于等于该年龄的对象直接晋升。

  • 空间担保: Survivor 区放不下本次存活的对象时,借用老年代空间存放。

  • 3. 老年代与 Full GC

    老年代存放的是生命周期长、存活率高的对象,极其适合标记-清除或标记-整理算法。 当老年代空间不足时,会触发 Full GC,对整个堆(新生代 + 老年代)进行彻底清理。Full GC 的 STW 时间极长,是系统卡顿的罪魁祸首之一。如果清理后依然没有空间,就会无情抛出 OutOfMemoryError。

    五、垃圾回收器(CMS vs G1)

    有了理论和策略,我们需要具体的干活工具。随着技术演进,垃圾回收器从单线程(Serial)走向多线程并行(Parallel),最终迎来了并发时代(用户线程与 GC 线程同时运行)。

    CMS (Concurrent Mark Sweep)

    低停顿:在进行垃圾回收时,尽可能缩短应用程序暂停的时间。让应用线程能够更快地恢复执行。

    CMS是一款以低停顿为目标的老年代垃圾收集器,基于“标记-清除”算法。

    工作流程:

  • 初始标记(STW):仅标记 GC Roots 直接关联的对象。因为范围小,所以速度极快。

  • 并发标记:对初始标记的对象进行整个引用链的扫描。这是最耗时的阶段,但允许与用户线程并发执行,从而大幅降低停顿。

  • 重新标记(STW):修正并发标记期间因用户线程运行导致的引用变动(漏标问题)。虽然需要暂停,但耗时远短于并发标记。

  • 并发清除:将标记为垃圾的对象进行清除。

  • CMS为啥可以做到低停顿呢?

    CMS将原本冗长的引用链扫描进行切分。通过 GC 线程与用户线程并发执行,加上重新标记校正的方式,减少了垃圾回收的时间

    缺点:

    • 使用了标记清除算法,会有内存碎片;

    • 垃圾回收和用户线程同时进行,会产生浮动垃圾


    G1 (Garbage-First)

    G1 是面向整个堆的垃圾回收器,同时管理新生代和老年代。

    G1 最大的特点是把整个堆划分成大量 Region,每个 Region 可以动态扮演 Eden、Survivor 或 Old 区的角色。回收时不再以整个代作为单位,而是以 Region 作为回收单位。

    工作流程:

  • 初始标记(STW):标记 GC Roots 直接关联的对象,耗时极短。

  • 并发标记:从初始标记的根对象出发, 遍历全堆所有 Region, 标记存活对象

  • 重新标记(STW):修正并发标记期间因用户线程运行导致的引用变动(漏标问题)。虽然需要暂停,但耗时远短于并发标记。

  • 筛选回收(STW):这是 G1 的核心。它会统计各 Region 的垃圾价值并排序,根据用户设定的最大停顿时间目标(-XX:MaxGCPauseMillis),优先回收“性价比”最高(垃圾最多)的 Region。回收时采用复制算法,宏观上实现了标记-整理,彻底解决了内存碎片问题。】


  • CMS 与 G1 的区别

    第一,CMS 只负责老年代回收,而 G1 管理整个堆。

    第二,CMS 采用标记清除算法,会产生内存碎片;G1 采用复制整理思想,基本不会产生碎片。

    第三,CMS 是对整个老年代进行回收;G1 是以 Region 为单位进行选择性回收。

    赞(0)
    未经允许不得转载:171主机测评 » 别焦虑 AI 抢饭碗了!先看看 JVM 是如何对你代码里的垃圾对象进行“物理超度”的
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址