Skip to main content
垃圾回收(Garbage Collection,GC)是 JVM 自动管理内存的机制,负责回收不再使用的对象所占用的内存。

如何判断对象可回收

引用计数法

给对象添加一个引用计数器,每当有引用指向它时加 1,引用失效时减 1,计数为 0 时可回收。
// 引用计数法的问题:循环引用
class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;  // a 引用 b
b.next = a;  // b 引用 a
a = null;
b = null;
// a 和 b 相互引用,计数都不为 0,但都无法访问
引用计数法无法解决循环引用问题,JVM 不使用此方法。

可达性分析

GC Roots 出发,向下搜索,搜索走过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链相连,则该对象可回收。
GC Roots

    ├── Object 1 ──→ Object 2
    │       │
    │       └────→ Object 3

    └── Object 4

Object 5 ──→ Object 6  ← 不可达,可回收

GC Roots

可以作为 GC Roots 的对象:
类型说明
虚拟机栈中的引用栈帧中的局部变量表引用的对象
方法区中的静态属性类的静态变量引用的对象
方法区中的常量常量池中引用的对象
Native 方法引用的对象JNI 引用的对象
同步锁持有的对象synchronized 锁住的对象

引用类型

JDK 1.2 后,Java 将引用分为四种类型:

强引用(Strong Reference)

最常见的引用,只要强引用存在,对象就不会被回收。
Object obj = new Object();  // 强引用
obj = null;  // 断开引用后可回收

软引用(Soft Reference)

内存不足时才会被回收,常用于缓存。
import java.lang.ref.SoftReference;

Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null;

// 获取对象
Object o = softRef.get();  // 可能返回 null

弱引用(Weak Reference)

下次 GC 时一定会被回收。
import java.lang.ref.WeakReference;

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

System.gc();
Object o = weakRef.get();  // null

虚引用(Phantom Reference)

最弱的引用,无法通过虚引用获取对象,主要用于跟踪对象被回收的状态。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
obj = null;

phantomRef.get();  // 始终返回 null

引用类型对比

引用类型回收时机使用场景
强引用从不回收普通引用
软引用内存不足时缓存
弱引用下次 GCWeakHashMap
虚引用任何时候跟踪回收状态

垃圾回收算法

标记-清除(Mark-Sweep)

标记阶段:标记所有可达对象
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ ✓ │   │ ✓ │   │ ✓ │   │   │ ✓ │
    └───┴───┴───┴───┴───┴───┴───┴───┘

清除阶段:回收未标记的对象
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ ✓ │   │ ✓ │   │ ✓ │   │   │ ✓ │
    └───┴───┴───┴───┴───┴───┴───┴───┘
    
问题:产生内存碎片
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ A │   │ B │   │ C │   │   │ D │
    └───┴───┴───┴───┴───┴───┴───┴───┘
优点:实现简单 缺点:产生内存碎片

复制算法(Copying)

将内存分为两块,每次只使用一块,GC 时将存活对象复制到另一块。
使用中:
    ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
    │ A │   │ B │ C │ │   │   │   │   │
    └───┴───┴───┴───┘ └───┴───┴───┴───┘
          From              To

GC 后:
    ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
    │   │   │   │   │ │ A │ B │ C │   │
    └───┴───┴───┴───┘ └───┴───┴───┴───┘
          From              To
优点:无内存碎片,效率高 缺点:内存利用率只有 50%

标记-整理(Mark-Compact)

标记后将存活对象向一端移动,然后清理边界外的内存。
标记阶段:
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ A │   │ B │   │ C │   │   │ D │
    └───┴───┴───┴───┴───┴───┴───┴───┘

整理阶段:
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ A │ B │ C │ D │   │   │   │   │
    └───┴───┴───┴───┴───┴───┴───┴───┘
优点:无内存碎片,内存利用率高 缺点:移动对象有开销

分代收集

根据对象存活周期不同,使用不同的算法。
┌─────────────────────────────────────────────────────┐
│                     年轻代                           │
│   使用复制算法(对象存活率低,复制成本小)             │
│   ┌────────────┐  ┌─────┐  ┌─────┐                 │
│   │   Eden     │  │ S0  │  │ S1  │                 │
│   └────────────┘  └─────┘  └─────┘                 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│                     老年代                           │
│   使用标记-清除或标记-整理(对象存活率高)             │
└─────────────────────────────────────────────────────┘

垃圾收集器

新生代收集器

Serial 收集器

单线程收集器,简单高效,适合单核 CPU。
用户线程 ────────────┬─────────┬────────────────
                    │  暂停   │
GC 线程             │ ═══════ │
                    │ Serial  │
-XX:+UseSerialGC

ParNew 收集器

Serial 的多线程版本,常与 CMS 配合使用。
用户线程 ────────────┬─────────┬────────────────
                    │  暂停   │
GC 线程 1           │ ═══════ │
GC 线程 2           │ ═══════ │
GC 线程 3           │ ═══════ │
-XX:+UseParNewGC

Parallel Scavenge 收集器

关注吞吐量,适合后台运算任务。
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=100    # 最大停顿时间
-XX:GCTimeRatio=99          # 吞吐量(1/(1+99)=1%)

老年代收集器

Serial Old 收集器

Serial 的老年代版本,使用标记-整理算法。

Parallel Old 收集器

Parallel Scavenge 的老年代版本。
-XX:+UseParallelOldGC

CMS 收集器

以最短回收停顿时间为目标,使用标记-清除算法。
用户线程 ─────┬────┬─────────────┬────┬─────
             │初始│             │重新│
             │标记│             │标记│
GC 线程      │    │ 并发标记    │    │ 并发清除
                  └─────────────┘    └──────
四个阶段:
  1. 初始标记:STW,标记 GC Roots 直接关联的对象
  2. 并发标记:与用户线程并发,标记可达对象
  3. 重新标记:STW,修正并发标记期间的变动
  4. 并发清除:与用户线程并发,清除垃圾
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75  # 老年代使用率达到 75% 触发
CMS 缺点
  • CPU 敏感
  • 产生浮动垃圾
  • 内存碎片

G1 收集器(JDK 9 默认)

面向服务端,低延迟、可预测的停顿时间。

Region 划分

┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ S │ O │ O │ H │ O │ E │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ E │ O │ O │ E │ S │ O │ O │ E │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ O │ O │ E │ E │ E │ O │ H │ O │
└───┴───┴───┴───┴───┴───┴───┴───┘

E = Eden    S = Survivor    O = Old    H = Humongous

工作流程

  1. 初始标记:STW,标记 GC Roots 直接关联的对象
  2. 并发标记:与用户线程并发
  3. 最终标记:STW,处理并发阶段遗留的 SATB 记录
  4. 筛选回收:STW,按回收价值排序,回收部分 Region
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200      # 期望最大停顿时间
-XX:G1HeapRegionSize=2m       # Region 大小

ZGC(JDK 11+)

超低延迟收集器,停顿时间不超过 10ms。
-XX:+UseZGC

收集器对比

收集器新生代/老年代算法线程特点
Serial新生代复制单线程简单高效
ParNew新生代复制多线程配合 CMS
Parallel Scavenge新生代复制多线程吞吐量优先
Serial Old老年代标记-整理单线程
Parallel Old老年代标记-整理多线程吞吐量优先
CMS老年代标记-清除并发低延迟
G1全堆标记-整理+复制并发可控停顿
ZGC全堆标记-整理并发超低延迟

GC 类型

类型说明
Minor GC / Young GC新生代 GC
Major GC / Old GC老年代 GC
Full GC整堆 GC(包括方法区)

触发条件

Minor GC:Eden 区满 Full GC
  • 老年代空间不足
  • 方法区空间不足
  • System.gc() 调用
  • Minor GC 后存活对象大于老年代剩余空间

小结

  • 可达性分析:从 GC Roots 出发,不可达的对象可回收
  • 引用类型:强 > 软 > 弱 > 虚,影响回收时机
  • 算法:标记-清除、复制、标记-整理、分代收集
  • 收集器:Serial、ParNew、Parallel、CMS、G1、ZGC
  • 选择建议
    • 小内存、单核:Serial
    • 吞吐量优先:Parallel
    • 低延迟:CMS、G1
    • 超低延迟:ZGC