说说你对JAVA垃圾回收的理解
本文讲解Java GC:一种自动内存管理机制。它通过可达性分析识别垃圾,采用分代收集策略,结合标记-清除、复制、整理等算法回收内存。现代回收器如G1、ZGC致力于降低STW(Stop-the-World)停顿时间。
我们来全面且深入地探讨一下Java的垃圾回收(Garbage Collection, GC)机制。这套机制是Java语言的核心优势之一,它将程序员从繁琐的手动内存管理中解放出来。
我会从以下几个方面来为你讲解:
- GC是什么,为什么需要它? (What & Why)
- 如何判断对象是“垃圾”? (The "How" - Identification)
- 回收“垃圾”的常用算法是什么? (The "How" - Collection Algorithms)
- 现代JVM如何实现GC?——分代收集 (The "How" - Practical Implementation)
- 有哪些经典的垃圾回收器? (The "Tools" - Collectors)
- 一个关键概念: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) 的算法来判断对象是否存活。
基本思想:
- GC Roots: 算法首先确定一系列必须存活的“根”对象,它们是垃圾回收的起点。
- 引用链: 从这些GC Roots开始,沿着对象之间的引用关系向下搜索,形成一条条“引用链”(Reference Chain)。
- 判断: 如果一个对象能够通过任何一条引用链最终与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后仍然存活的对象,或者一些体积较大的对象。
- 特点: 对象存活率高,生命周期长。
- 回收算法: 采用标记-清除或标记-整理算法。因为存活对象多,复制成本高,不适合用复制算法。
对象晋升过程:
- 出生: 绝大多数新对象在Eden区分配。
- Minor GC (或 Young GC): 当Eden区满时,触发一次新生代GC。
- 幸存: Eden区中存活的对象会被复制到其中一个Survivor区(比如S0),并且对象的年龄(Age)+1。Eden区被清空。
- 再次GC: 当Eden区再次满时,会触发又一次Minor GC。这次GC会清理Eden区和之前那个Survivor区(S0)。所有存活的对象(来自Eden和S0)会被复制到另一个Survivor区(S1),年龄+1。然后清空Eden和S0。
- 交替: S0和S1的角色会不断互换,每次Minor GC后,总有一个Survivor区是空的。
- 晋升老年代: 当一个对象的年龄达到某个阈值(默认为15)后,它就会被“晋升”(Promote)到老年代。
- 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)
- 缺点: 会产生内存碎片,且在并发阶段会占用CPU资源。在JDK 9中被标记为废弃。(
- G1 (Garbage-First): 一款面向服务端的、可预测停顿时间的垃圾回收器。JDK 9及以后的默认回收器。
- 特点: 它将整个堆划分为多个大小相等的独立区域(Region),并跟踪每个Region里垃圾的价值。回收时,优先选择垃圾最多的Region进行回收,这也是“Garbage-First”的由来。它兼顾了吞吐量和低延迟。(
-XX:+UseG1GC)
- 特点: 它将整个堆划分为多个大小相等的独立区域(Region),并跟踪每个Region里垃圾的价值。回收时,优先选择垃圾最多的Region进行回收,这也是“Garbage-First”的由来。它兼顾了吞吐量和低延迟。(
- 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垃圾回收机制的核心。