基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

详细讲讲 Java 中垃圾回收的完整过程

Java 中的垃圾回收(Garbage Collection, 简称 GC)是一个非常庞大且精密的系统。要完整理解整个过程,我们需要从“如何发现垃圾”“垃圾存放的区域划分”“对象的一生(回收流转过程)”以及“底层回收算法”四个维度来剖析。

以下是 Java 垃圾回收的完整、详细的过程解析:


第一步:如何判定一个对象是“垃圾”?

在回收之前,JVM 必须知道哪些对象还在用,哪些已经没用了。Java 主要使用可达性分析算法(Reachability Analysis),而不是引用计数法(因为引用计数法无法解决循环引用的问题)。

  1. 确定 GC Roots(根对象):
    JVM 会定义一组“绝对不能被回收”的对象作为起点,称为 GC Roots。常见的 GC Roots 包括:

    • 虚拟机栈(线程局部变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI(Native方法)引用的对象。
    • 活跃线程本身。
  2. 引用链追踪:
    从 GC Roots 开始向下搜索,搜索走过的路径称为“引用链”。

    • 存活对象: 如果一个对象到 GC Roots 之间有引用链相连(即从 GC Roots 可以“到达”该对象),说明它还在被使用。
    • 垃圾对象: 如果一个对象到所有的 GC Roots 都没有引用链相连(不可达),那么它就会被判定为垃圾。

第二步:内存区域的划分(分代假说)

Java 的垃圾回收主要发生在堆(Heap)内存中。JVM 设计者基于“弱分代假说”(绝大多数对象都是朝生夕灭的,熬过越多次垃圾回收的对象越难死亡),将堆内存进行了划分:

  • 新生代(Young Generation): 存放新创建的对象。占堆内存的 1/3。它又被细分为三个区:
    • Eden 区(伊甸园):占新生代的 80%。
    • Survivor 0 区(也叫 From 区):占新生代的 10%。
    • Survivor 1 区(也叫 To 区):占新生代的 10%。
  • 老年代(Old Generation): 存放经过多次 GC 依然存活的对象,或者占用内存极大的大对象。占堆内存的 2/3。

(注:JDK 8 之后的元空间 Metaspace 也会发生 GC,主要回收废弃的类和常量,但核心的 GC 发生在堆中)


第三步:垃圾回收的完整流转过程(对象的一生)

这是 GC 最核心的动态过程。我们以一个普通对象的创建为例:

1. 对象的诞生(分配在 Eden 区)
当你 new 一个对象时,JVM 优先将其分配在新生代的 Eden 区

2. 第一次 Minor GC(新生代 GC)
随着程序的运行,Eden 区逐渐被填满。当 Eden 区没有足够空间分配新对象时,触发 Minor GC

  • JVM 通过可达性分析找出 Eden 区和 Survivor From 区(此时可能为空)的存活对象。
  • 将这些存活对象复制到 Survivor To 区。
  • 存活对象的分代年龄加 1(初始为 1)。
  • 清空 Eden 区和 Survivor From 区的所有垃圾对象。
  • 身份互换: 原来的 To 区变成 From 区,原来的 From 区变成 To 区(保证始终有一个 Survivor 区是空的)。

3. 对象在新生代中熬过多次 GC
随后 Eden 区再次填满,再次触发 Minor GC。

  • 此时,存活对象不仅来自 Eden,还来自上一次的 Survivor From 区。
  • 它们会被一起复制到当前的 Survivor To 区,年龄再次加 1。
  • 如此往复,大部分对象在年轻代就被回收了。

4. 晋升到老年代(Promotion)
并不是所有对象都会一直留在新生代,满足以下条件的对象会被移入老年代:

  • 年龄达标: 默认情况下,对象在 Survivor 区中每熬过一次 Minor GC,年龄加 1。当年龄达到 15(由 -XX:MaxTenuringThreshold 控制)时,晋升老年代。
  • 大对象直接进入老年代: 需要大量连续内存空间的对象(如超大数组),为了避免在 Eden 和两个 Survivor 之间来回复制产生大量开销,直接在老年代分配。
  • 动态年龄判断: 如果 Survivor 区中相同年龄的所有对象大小的总和,大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象直接进入老年代。

5. 老年代满了:触发 Major GC / Full GC
随着对象不断晋升,老年代的内存也会被逐渐填满。当老年代空间不足时,会触发老年代的垃圾回收。

  • Major GC: 仅回收老年代。
  • Full GC: 回收整个堆,包括新生代、老年代和元空间。
  • STW(Stop-The-World): Full GC 极其消耗时间,发生时会导致所有用户线程暂停(即系统卡顿)。因此,JVM 调优的一个核心目标就是减少 Full GC 的次数

第四步:底层的垃圾回收算法

垃圾具体是怎么被清理掉的?主要依靠以下三种基础算法:

  1. 标记-清除算法(Mark-Sweep)
    • 过程: 标记出所有存活对象,然后直接把未标记的垃圾对象清理掉。
    • 缺点: 会产生大量的内存碎片。如果此时来了一个大对象,虽然总剩余空间足够,但没有连续空间,会导致提前触发下一次 GC。
  2. 复制算法(Copying)
    • 过程: 将内存一分为二,每次只用其中一半。GC 时,把存活的对象连续地复制到另一半去,然后把原来那一半全部清空。
    • 优点: 效率高,没有内存碎片。
    • 缺点: 浪费了一半的内存。
    • 应用: 新生代采用的就是这种算法的优化版(Eden 80% + S0 10% + S1 10%,只浪费 10% 的空间)。
  3. 标记-整理算法(Mark-Compact)
    • 过程: 标记出所有存活对象,然后让所有存活的对象都向内存空间的一端移动,最后直接清理掉边界以外的内存。
    • 优点: 没有内存碎片,也不浪费空间。
    • 缺点: 移动对象需要时间,效率相对较低。
    • 应用: 适合存活率高的老年代

第五步:现代 JVM 的垃圾收集器(谁来执行 GC?)

上述算法是理论,真正执行这些算法的是垃圾收集器(Garbage Collector)。Java 发展至今,诞生了多种收集器:

  • 早期组合:Serial / Parallel Scavenge 等
    • 依靠上述严格的物理分代模型工作。Parallel 是关注系统吞吐量的收集器(JDK 8 默认)。
  • CMS 收集器(Concurrent Mark Sweep):
    • 老年代收集器,以获取最短回收停顿时间(低延迟)为目标。它允许在垃圾回收的某些阶段,用户线程和 GC 线程并发执行,减少了 STW 时间。
  • G1 收集器(Garbage-First):
    • JDK 9 以后的默认收集器。它打破了物理上的严格分代,将整个堆划分成多个大小相等的独立区域(Region)。它可以预测停顿时间,优先回收垃圾最多的 Region(这也是 Garbage-First 名字的由来),非常适合大内存的多核服务器。
  • ZGC / Shenandoah(新一代收集器):
    • 致力于实现超低延迟(STW 停顿时间不超过 1ms 到 10ms),即使在 TB 级别的堆内存下也能保持极低的停顿时间,几乎所有过程都与用户线程并发执行。

总结

Java 垃圾回收的完整过程可以概括为:

  1. 利用 GC Roots 找到活跃对象,剩下的就是垃圾。
  2. 新对象在 Eden 区 诞生。
  3. 在年轻代通过 复制算法 进行快速、频繁的 Minor GC
  4. 熬过多次 GC 的对象晋升到 老年代
  5. 老年代满了则触发 Full GC,通常使用 标记-整理 算法,会导致程序暂停(STW)。
  6. 现代 GC 收集器(如 G1, ZGC)正朝着打破传统分代、极低延迟的方向发展。
00:00
00:00