基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

说说你对JAVA垃圾回收的理解

知识点图片

本文讲解Java GC:一种自动内存管理机制。它通过可达性分析识别垃圾,采用分代收集策略,结合标记-清除、复制、整理等算法回收内存。现代回收器如G1、ZGC致力于降低STW(Stop-the-World)停顿时间。

我们来全面且深入地探讨一下Java的垃圾回收(Garbage Collection, GC)机制。这套机制是Java语言的核心优势之一,它将程序员从繁琐的手动内存管理中解放出来。

我会从以下几个方面来为你讲解:

  1. GC是什么,为什么需要它? (What & Why)
  2. 如何判断对象是“垃圾”? (The "How" - Identification)
  3. 回收“垃圾”的常用算法是什么? (The "How" - Collection Algorithms)
  4. 现代JVM如何实现GC?——分代收集 (The "How" - Practical Implementation)
  5. 有哪些经典的垃圾回收器? (The "Tools" - Collectors)
  6. 一个关键概念:Stop-the-World (STW)

1. GC是什么,为什么需要它?

是什么 (What):
Java的垃圾回收机制是一种自动内存管理系统。它在程序运行期间,自动地、持续地监控由new关键字创建的对象,并识别出那些不再被任何“活”的线程引用的对象(即“垃圾”),然后自动释放这些对象所占用的内存空间。

为什么需要 (Why):
在像C/C++这样的语言中,开发者需要手动管理内存。使用malloc()/new分配内存后,必须在适当的时候使用free()/delete来释放它。这会带来两个主要问题:

  • 内存泄漏 (Memory Leak): 忘记释放不再使用的内存,导致这部分内存永远无法被再次使用,程序最终可能因内存耗尽而崩溃。
  • 悬空指针 (Dangling Pointer): 内存已经被释放,但仍然有指针指向这块内存。此时再通过这个指针访问内存,会导致未定义的行为,是严重的安全隐患。

Java的GC机制通过自动化这个过程,极大地提高了开发的效率和程序的健壮性,让程序员可以更专注于业务逻辑。


2. 如何判断对象是“垃圾”?

JVM使用一种叫做可达性分析 (Reachability Analysis) 的算法来判断对象是否存活。

基本思想:

  1. GC Roots: 算法首先确定一系列必须存活的“根”对象,它们是垃圾回收的起点。
  2. 引用链: 从这些GC Roots开始,沿着对象之间的引用关系向下搜索,形成一条条“引用链”(Reference Chain)。
  3. 判断: 如果一个对象能够通过任何一条引用链最终与GC Roots相连,那么它就是可达的 (Reachable),意味着它是“活”的,不能被回收。反之,如果一个对象到所有GC Roots都不可达,那它就是“垃圾”,可以被回收。

哪些可以作为GC Roots?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象: 即当前正在执行的方法里的局部变量所引用的对象。
  • 方法区中类静态属性引用的对象:static关键字修饰的字段引用的对象。
  • 方法区中常量引用的对象: 例如字符串常量池里的引用。
  • 本地方法栈(JNI)中引用的对象: Native方法(通常由C/C++编写)引用的Java对象。
  • 被同步锁(synchronized)持有的对象。

注意: JVM并未使用“引用计数法”(Reference Counting)。该算法虽然简单,但无法解决对象之间循环引用的问题(例如 A.ref = B; B.ref = A;),而可达性分析则可以完美解决。


3. 回收“垃圾”的常用算法是什么?

确定了垃圾之后,下一步就是如何回收。主要有三种基础算法:

a. 标记-清除 (Mark-Sweep)

这是最基础的算法。

  • 标记 (Mark): 从GC Roots开始,遍历所有可达对象,并给它们打上“存活”标记。
  • 清除 (Sweep): 再次遍历整个堆内存,将所有未被标记的对象(即垃圾)进行回收,清除其所占空间。

优点: 实现简单。
缺点:

  • 效率问题: 需要两次遍历,效率不高。
  • 空间碎片化: 清除后会产生大量不连续的内存碎片。如果后续需要分配一个较大的对象,可能因为找不到足够大的连续空间而触发另一次GC。

b. 标记-复制 (Mark-Copy / Copying)

为了解决碎片化问题,该算法出现了。

  • 划分: 将可用内存按容量划分为大小相等的两块,每次只使用其中一块。
  • 回收过程: 当这一块内存用完时,就将还存活着的对象复制到另一块上面,然后把已使用过的那块内存空间一次性全部清理掉

优点:

  • 无碎片: 实现简单,运行高效,不会产生内存碎片。
  • 效率高: 只需移动存活对象,对于存活对象很少的场景,效率极高。

缺点:

  • 空间浪费: 可用内存缩小为原来的一半,代价太大。

c. 标记-整理 (Mark-Compact)

结合了前两种算法的优点。

  • 标记 (Mark): 过程与“标记-清除”一样,先标记出所有存活对象。
  • 整理 (Compact): 不是直接清理,而是将所有存活对象都向内存空间的一端移动,然后直接清理掉端边界以外的内存。

优点:

  • 无碎片: 解决了碎片化问题。
  • 无空间浪费: 不像复制算法那样浪费一半空间。

缺点:

  • 效率较低: 不仅要标记,还要移动所有存活对象,成本较高。

4. 现代JVM如何实现GC?——分代收集 (Generational Collection)

上述三种算法各有优劣。实践中,JVM的实现者发现了一个重要的经验法则:“绝大多数对象都是朝生夕死的”

基于这个特点,现代JVM(如HotSpot)采用了分代收集策略,将堆内存划分为不同的区域,并为每个区域选择最合适的回收算法。

堆内存划分:

  • 新生代 (Young Generation):

    • 存放新创建的对象。绝大多数对象在这里被创建,并很快变得不可达。
    • 特点: 对象存活率低。
    • 回收算法: 采用标记-复制算法。因为存活对象少,复制成本低,效率极高。
    • 新生代内部又细分为:
      • Eden区 (80%): 新对象出生的地方。
      • 两个Survivor区 (各10%, From/To): 用于存放经过一次GC后仍然存活的对象。
  • 老年代 (Old Generation):

    • 存放经过多次新生代GC后仍然存活的对象,或者一些体积较大的对象。
    • 特点: 对象存活率高,生命周期长。
    • 回收算法: 采用标记-清除标记-整理算法。因为存活对象多,复制成本高,不适合用复制算法。

对象晋升过程:

  1. 出生: 绝大多数新对象在Eden区分配。
  2. Minor GC (或 Young GC): 当Eden区满时,触发一次新生代GC。
  3. 幸存: Eden区中存活的对象会被复制到其中一个Survivor区(比如S0),并且对象的年龄(Age)+1。Eden区被清空。
  4. 再次GC: 当Eden区再次满时,会触发又一次Minor GC。这次GC会清理Eden区和之前那个Survivor区(S0)。所有存活的对象(来自Eden和S0)会被复制到另一个Survivor区(S1),年龄+1。然后清空Eden和S0。
  5. 交替: S0和S1的角色会不断互换,每次Minor GC后,总有一个Survivor区是空的。
  6. 晋升老年代: 当一个对象的年龄达到某个阈值(默认为15)后,它就会被“晋升”(Promote)到老年代
  7. Major GC (或 Full GC): 当老年代空间不足时,会触发一次Major GC,对老年代甚至整个堆进行回收。这个过程通常更慢。

5. 有哪些经典的垃圾回收器?

垃圾回收器是GC算法的具体实现。没有“最好”的,只有“最合适”的。

  • Serial GC: 单线程回收器。GC时,所有用户线程都必须暂停。适用于客户端模式或内存较小的应用。(-XX:+UseSerialGC)
  • Parallel GC (吞吐量优先): 多线程版本的Serial GC。GC时,多个线程并行工作,能有效缩短GC停顿时间。JDK 8的默认回收器。适用于后台计算、数据处理等对吞吐量要求高的场景。(-XX:+UseParallelGC)
  • CMS (Concurrent Mark Sweep, 低延迟优先): 以获取最短回收停顿时间为目标的回收器。它在标记和清除阶段的大部分工作都可以和用户线程并发执行。
    • 缺点: 会产生内存碎片,且在并发阶段会占用CPU资源。在JDK 9中被标记为废弃。(-XX:+UseConcMarkSweepGC)
  • G1 (Garbage-First): 一款面向服务端的、可预测停顿时间的垃圾回收器。JDK 9及以后的默认回收器
    • 特点: 它将整个堆划分为多个大小相等的独立区域(Region),并跟踪每个Region里垃圾的价值。回收时,优先选择垃圾最多的Region进行回收,这也是“Garbage-First”的由来。它兼顾了吞吐量和低延迟。(-XX:+UseG1GC)
  • ZGC 和 Shenandoah: 最新的超低延迟回收器。它们的目标是将GC停顿时间控制在毫秒甚至亚毫秒级别,几乎所有工作都与用户线程并发执行。适用于对延迟极其敏感的大内存(几十G甚至上百G)应用。

6. 一个关键概念:Stop-the-World (STW)

无论选择哪种GC算法或回收器,都有一个不可避免的阶段,那就是Stop-the-World (STW)

STW是指在执行垃圾回收时,必须暂停所有正在运行的Java应用程序线程。 就像是“时间静止”了一样,只有GC线程在工作。

为什么需要STW?
为了保证GC过程的正确性。如果在GC线程标记对象的过程中,应用程序线程还在不断地修改对象引用关系,那么GC得到的结果就会不准确(比如把“活”对象当成“垃圾”回收了),导致严重错误。所以需要一个“快照”时刻,这个时刻就是通过STW来保证的。

所有GC优化的核心目标之一就是缩短STW的时间和降低其发生的频率。像CMS、G1、ZGC等现代回收器,就是通过复杂的并发技术,将大量原本需要在STW中完成的工作,拿出来与用户线程并发执行,从而极大地缩短了STW的时间。

总结

  • 目的: 自动管理内存,避免内存泄漏和悬空指针。
  • 识别垃圾: 通过可达性分析,从GC Roots出发,找不到的对象即为垃圾。
  • 回收算法: 核心有标记-清除(有碎片)、标记-复制(浪费空间)、标记-整理(效率稍低)。
  • 现代实现: 采用分代收集策略,新生代用复制算法,老年代用标记-清除或标记-整理。
  • 回收器: 从Serial到Parallel,再到CMS、G1,以及最新的ZGC,不断向着高吞吐量低延迟的目标进化。
  • 核心痛点: Stop-the-World (STW),所有现代GC器都在努力减少STW的影响。

理解了这些,你就掌握了Java垃圾回收机制的核心。

00:00
00:00