GC 垃圾回收

#GC #JAVA #JVM

GC(Garbage Collection)是 JVM 中实现自动内存管理的一类机制的统称,其具体实现包括 Parallel、G1、ZGC 等垃圾回收器。GC 主要作用于堆(Heap)内存——在新生代执行 Minor GC,在老年代触发 Major GC 或 Full GC****。虽然 GC 不直接管理直接内存,但会间接影响其回收(例如通过 GC 触发 Cleaner 释放 DirectByteBuffer)。在 JVM 架构中,GC 与运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地方法接口(Native Method Interface)等组件协同工作,共同支撑 Java 程序的运行。

什么是可被回收的,如何判断?


引用计数器

通过一个计数器标记一个对象被引用的次数来判断是否进行垃圾回收,当引用数量为 0 时在垃圾回收时将会进行回收.

问题: 出现循环引用时将导致无法释放内存.

如 A 引用 B,B 引用 A.

循环引用

可达性分析算法

GC 会从一组“GC 根”(GCRoots)出发,沿着引用链一路向下找到所有可达对象。这些可达对象被认为是“存活”的,不会被回收。其他不可达的对象(被称为“可回收对象”)则有资格被回收。

GC Roots

GC Roots 是一组在Java虚拟机中被直接引用的对象,是垃圾收集器从它们开始遍历所有可达对象(即存活对象)的起点

来源包括(但不限于):

  • 栈中的引用(局部变量、方法参数)
public void method() {
    Object localObj = new Object(); // localObj 是 GC Root
    String str = "hello";           // str 是 GC Root
    // 这些变量在栈帧中,直接引用堆中的对象
}

public void process(Object param) {
    // param 是 GC Root,引用传入的对象
}

public String concat(String a, String b) {
    return a + b; // 编译器可能生成临时变量引用中间结果
}
  • Meta Space 中的静态 (static) 变量 和 常量引用对象
public class MyClass {
    private static Object staticField = new Object(); // 静态字段是 GC Root
    public static final String CONSTANT = "constant"; // 静态常量也是 GC Root
}

// 即使没有 MyClass 的实例,staticField 仍然可达

public class Constants {
    // 字符串常量池中的对象
    String s1 = "literal"; // "literal" 对象被常量池引用,是 GC Root
    String s2 = new String("created"); // "created" 也在常量池中
}
  • JNI 环境引用
// C/C++ 本地代码
JNIEXPORT void JNICALL Java_MyClass_nativeMethod
  (JNIEnv *env, jobject obj) {
    // 通过 JNI 创建的全局引用或局部引用
    // 这些引用的对象都是 GC Root
    jobject globalRef = (*env)->NewGlobalRef(env, someObject);
}

public class HeapHprofBinWriter extends AbstractHeapGraphWriter {

    // hprof binary file header
    private static final String HPROF_HEADER_1_0_2 = "JAVA PROFILE 1.0.2";
    //    .....
    private static final int HPROF_GC_ROOT_UNKNOWN       = 0xFF;
    private static final int HPROF_GC_ROOT_JNI_GLOBAL    = 0x01;
    private static final int HPROF_GC_ROOT_JNI_LOCAL     = 0x02;
    private static final int HPROF_GC_ROOT_JAVA_FRAME    = 0x03;
    private static final int HPROF_GC_ROOT_NATIVE_STACK  = 0x04;
    private static final int HPROF_GC_ROOT_STICKY_CLASS  = 0x05;
    private static final int HPROF_GC_ROOT_THREAD_BLOCK  = 0x06;
    private static final int HPROF_GC_ROOT_MONITOR_USED  = 0x07;
    private static final int HPROF_GC_ROOT_THREAD_OBJ    = 0x08;
    //    .....
}

方法区回收(或Meta Space)

finalize()

GC 主要作用范围

Heap

堆内的垃圾收集算法主要基于分代收集法,根据对象的生命周期长短将内存分为年轻代和老年代,分别采用不同的收集策略,以提高垃圾收集的效率。

部分 GC

部分 GC (Partial GC) 是指不完整的垃圾收集,只针对堆的特定部分进行收集。主要包括 Young GC 和 Old GC。

Young GC

仅作用于 Young generation 的垃圾收集,每次将可回收内存标记回收,存活内存移入 Survivor 区域。由于大多数对象的生命周期较短,Young GC 发生频繁但速度较快。

Old GC (Major GC) 是指只针对老年代进行的垃圾收集。通常会导致应用程序暂停,但发生频率低于 Young GC。当老年代空间不足时触发。

Full GC

Full GC 是对整个堆进行垃圾收集,包括年轻代、老年代和元空间(JDK 8 之前称为永久代)。Full GC 的暂停时间通常较长,会导致应用程序长时间停顿,影响用户体验。

混合 GC (Mixed GC)

G1 收集器特有的收集方式,既会收集年轻代也会收集部分老年代。G1 通过将堆分成多个区域(Region),每次收集时可以有选择地收集部分区域,减少停顿时间。

MetaSpace

MetaSpace 使用的是本地内存,不在 JVM 堆中。它主要用于存储类的元数据信息,如类结构、方法数据、常量池等。MetaSpace 的垃圾收集通常在 Full GC 时进行。

引用类型


StrongReference

Java 中最常见最普通的引用方式,比如:

Foo foo = new Foo();

这个 foo 就是一个强引用。只要存在这样的强引用链路指向 Foo 对象,GC 就不会回收它,即使发生 OOM,

几乎所有日常编码用到的对象引用都是强引用。

SoftReference

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

如果一个对象只有软引用可达(即没有强引用链路指向它),JVM 并不一定立刻回收它。只有在内存紧张、垃圾回收器判断“需要腾出空间”的时候,才可能清除这些对象。

用途通常是缓存(cache)例如你希望保留一个对象作为缓存,但如果 JVM 内存紧张,希望它被垃圾回收掉,而不是导致 OOM。

WeakReference

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

如果一个对象 被弱引用可达(即没有强引用或软引用链路指向它),那么在下一次垃圾回收时,GC 就可以回收它,不要保留。换言之,弱引用对象是最容易被回收的那类。

PhantomReference

引用类型 可达性状态名称 是否阻止 GC get() 方法是否可得 被清除 / 入队列时机 / 特性 典型用途
Strong (强引用) “强可达”(strong reachable) 阻止 GC — 只要有强引用,GC 不会回收这个对象 —(不是通过 Reference 对象访问的) 不会自动被 GC 清除 日常使用、常规引用
SoftReference (软引用) “软可达”(soft reachable) 不阻止 GC,但 GC 在内存紧张时可能清除 get() 返回对象,除非已被清除 在内存不足时(JVM 判断需要)可被回收;若注册了 ReferenceQueue,则被清除后其引用对象会入队 缓存 (cache)、内存敏感的数据保留
WeakReference (弱引用) “弱可达”(weak reachable) 不阻止 GC — 一旦对象 只被弱引用 可达,就将被回收 get() 若对象未被回收则返回,否则返回 null 在下一次垃圾回收(或更早)就可被回收;若注册队列,则在清除/回收时入队 WeakHashMap、 canonicalizing map、避免内存泄漏辅助结构
PhantomReference (虚引用,也称幻影引用) “虚可达”(phantom reachable) 不阻止回收(比软弱还“弱”) get() 总是返回 null(永远不可通过它获取对象) 当对象“几乎要被回收”时被入队列(通常是在对象 finalize/清理之后) — 但 GC 在回收对象之前,必须先将其 phantom 引用入队(让程序得知该对象将被回收) 用于跟踪对象被回收的时刻,做清理 (cleanup)、资源释放、内存调度等

ReferenceQueue

垃圾回收算法

垃圾收集动图示例

标记-清除(Mark-Sweep)

  • 核心思想:两阶段完成回收。先从 GC Roots 出发“标记”所有存活对象,再“清除”未标记对象以回收其占用的内存。
  • 典型流程:
    1. Stop-The-World,遍历对象图进行可达性分析并标记存活对象
    2. 扫描堆空间,释放未标记对象所占内存
  • 优点:实现简单,无需移动大量对象
  • 缺点:
    • 会产生内存碎片,后续大对象分配可能失败而触发 Full GC
    • 清除阶段需要遍历堆,吞吐不佳
  • 适用场景:老年代的早期实现或碎片不敏感的场景

标记-整理(Mark-Compact)

  • 核心思想:同样先标记,再把存活对象“压缩”到一端,保持内存连续,最后清理边界之外的区域。
  • 典型流程:
    1. 标记存活对象
    2. 计算新地址并更新引用(有的实现先计算转发表)
    3. 将对象按新地址搬迁并整理出连续可用空间
  • 优点:避免内存碎片,便于后续大对象分配
  • 缺点:对象移动成本较高,需要更新引用,暂停时间可能更长
  • 适用场景:老年代常用策略,G1、ZGC 等也在混合或并发阶段引入压缩思想

复制(Copying)

  • 核心思想:将内存按比例分为两个对等或不对等的区域,每次只使用其中一个。GC 时把存活对象复制到“空闲”区域,随后整体清空原区域。
  • 典型流程:
    1. 新生代常见布局:Eden + S0 + S1(两个 Survivor)
    2. Minor GC 时从 Eden 和一个 Survivor 里复制存活对象到另一个 Survivor
    3. 完成后交换两个 Survivor 的角色
  • 优点:复制后空间天然连续,没有碎片,分配可用指针碰撞,速度快
  • 缺点:需要预留额外空间,内存利用率较低;存活率高时复制成本上升
  • 适用场景:新生代(对象朝生夕死,存活率低,复制效率高)

分代收集(Generational Collection)

  • 核心思想:依据“绝大多数对象朝生夕死”的经验法则,将堆划分为新生代与老年代,针对不同代采用不同算法和回收频率。
  • 典型实现:
    • 新生代:多用复制算法(Eden + Survivor),Minor GC 频繁且停顿短
    • 老年代:多用标记-清除或标记-整理,Major/Full GC 频率低但停顿长
    • 跨代引用:通过 Remembered Set 或 Card Table 跟踪老年代指向新生代的引用,避免每次扫描整个老年代
  • 优点:综合性能好,在典型负载下能显著降低总停顿时间
  • 缺点:需要维护跨代写屏障与集合结构,调参复杂
  • 适用场景:几乎所有 HotSpot 回收器的基础设计,如 Parallel、CMS(历史)、G1、Shenandoah、ZGC 等都体现了分代思想
  • 小结
    • 标记-清除:简单但易碎片化
    • 标记-整理:无碎片但移动成本高
    • 复制:快速无碎片,牺牲一部分空间
    • 分代:按对象存活特征选择算法,工程上最常见的组合策略

垃圾回收器

来源

详解Java的 GCRoot

JVM之垃圾回收机制(GC)

JVM垃圾回收详解(重点)

Types of References in Java

Java四种引用类型原理你真的搞明白了吗?五分钟带你深入理解!

说一说Java中的四种引用类型?