【JVM】GC垃圾回收
概念
在程序运行过程中,会不断创建对象来使用内存,当这些对象不再被引用时,其所占用的内存若不及时释放,会导致内存占用不断增加,最终可能引发内存溢出。GC 机制能自动检测并回收这些不再使用的对象所占用的内存。
JVM内存管理
在JVM中,运行时数据区由程序计数器、java虚拟机栈、本地方法栈、方法区、堆五部分组成。

其中程序计数器、java虚拟机栈、本地方法栈三部分是线程私有的,伴随着线程的创建而创建,线程的销毁而销毁,方法区的栈帧在执行完方法后就会弹出并释放对应的内存。
方法区的回收
1. 方法区/元空间会回收哪些数据?
方法区主要回收以下两类数据:
- 废弃的类(Unloaded Classes)
- 当一个类不再被任何地方引用(无实例对象、无ClassLoader加载、无反射调用等),JVM可以卸载该类。
- 卸载后,其类信息、方法字节码、运行时常量池等会被回收。
- 废弃的常量(Obsolete Constants)
- 字符串常量池(String Table)中的字符串,如果没有被引用,可以被回收。
- 运行时常量池(Runtime Constant Pool)中的符号引用(Symbolic References)也可以被回收。
2. 方法区的垃圾回收触发条件
(1) 类的卸载条件
一个类可以被卸载,必须满足 所有 以下条件:
- 该类所有的实例都已被回收(堆中无该类的对象)。
- 加载该类的
ClassLoader
已被回收(例如自定义类加载器被卸载)。 - 该类对应的
java.lang.Class
对象没有被任何地方引用(如反射、静态变量等)。
java
import java.lang.ref.WeakReference;
public class ClassUnloadDemo {
static WeakReference<Class<?>> weakClassRef;
public static void main(String[] args) throws Exception {
// 1. 创建自定义类加载器
CustomClassLoader loader = new CustomClassLoader();
// 2. 加载类
Class<?> clazz = loader.loadClass("TempClass");
weakClassRef = new WeakReference<>(clazz);
// 3. 创建实例
Object instance = clazz.getDeclaredConstructor().newInstance();
// 4. 检查初始状态
System.out.println("初始状态: " + (weakClassRef.get() != null ? "类存在" : "类已回收"));
// 5. 满足类卸载条件(分步骤演示)
System.out.println("\n=== 步骤1: 回收所有实例 ===");
instance = null;
System.gc();
Thread.sleep(100);
System.out.println("实例回收后: " + (weakClassRef.get() != null ? "类存在" : "类已回收"));
System.out.println("\n=== 步骤2: 回收ClassLoader ===");
loader = null;
System.gc();
Thread.sleep(100);
System.out.println("ClassLoader回收后: " + (weakClassRef.get() != null ? "类存在" : "类已回收"));
System.out.println("\n=== 步骤3: 释放Class引用 ===");
clazz = null;
System.gc();
Thread.sleep(100);
System.out.println("Class引用释放后: " + (weakClassRef.get() != null ? "类存在" : "类已回收"));
}
}
class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
// 简单模拟类字节码
byte[] b = new byte[100];
return defineClass(name, b, 0, b.length);
}
}
初始状态: 类存在
=== 步骤1: 回收所有实例 ===
实例回收后: 类存在
=== 步骤2: 回收ClassLoader ===
ClassLoader回收后: 类存在
=== 步骤3: 释放Class引用 ===
Class引用释放后: 类已回收
(2) 常量的回收条件
- 字符串常量:如果字符串常量池中的某个字符串不再被任何变量引用,可以被回收(类似弱引用)。
java
import java.lang.ref.WeakReference;
public class StringConstantGCDemo {
public static void main(String[] args) {
// 1. 创建字符串常量
String str = "HelloJVM"; // 进入常量池
WeakReference<String> weakRef = new WeakReference<>(str);
// 2. 初始状态
System.out.println("初始状态: " + weakRef.get());
// 3. 释放引用
System.out.println("\n释放引用后...");
str = null;
// 4. 尝试触发GC
for (int i = 0; i < 3; i++) {
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
System.out.println("GC后状态: " +
(weakRef.get() != null ? "字符串存在" : "字符串已回收"));
}
}
}
输出示例:
初始状态: HelloJVM
释放引用后...
GC后状态: 字符串存在
GC后状态: 字符串存在
GC后状态: 字符串已回收
说明:
字符串常量在没有强引用时可能被回收
回收时机取决于JVM实现(可能需要多次GC)
- 运行时常量池:如果某个符号引用(如类名、方法名)不再被使用,可以被回收。
java
import java.lang.reflect.Method;
import java.lang.ref.WeakReference;
public class SymbolUnloadDemo {
static WeakReference<Class<?>> weakClassRef;
public static void main(String[] args) throws Exception {
// 1. 加载类(创建符号引用)
Class<?> clazz = Class.forName("java.util.ArrayList");
weakClassRef = new WeakReference<>(clazz);
// 2. 使用方法(创建方法符号引用)
Method method = clazz.getMethod("size");
// 3. 初始状态
System.out.println("初始状态: " + weakClassRef.get().getSimpleName());
// 4. 释放引用
clazz = null;
method = null;
// 5. 触发GC
System.gc();
Thread.sleep(100);
// 6. 检查状态
System.out.println("GC后状态: " +
(weakClassRef.get() != null ? "符号引用存在" : "符号引用已回收"));
}
}
输出示例:
初始状态: ArrayList
GC后状态: 符号引用已回收
说明:
- 当类/方法不再被使用时,其符号引用可能被回收
- 符号引用回收通常发生在类卸载过程中
堆的回收
一、引用计数法和可达性分析算法
1.引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
缺点:
循环引用问题:
java
class Node {
Node next;
}
public static void main(String[] args) {
Node a = new Node(); // 计数=1
Node b = new Node(); // 计数=1
a.next = b; // b计数=2
b.next = a; // a计数=2
a = null; // a计数=1
b = null; // b计数=1
// 循环引用导致内存泄漏!
}
2.可达性分析算法
可达性分析算法将对象分为了两种:GC Root对象、普通对象。

可达性分析算法以GC Root的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root之间没有任何引用链项链时,则证明此对象是不可用的,可判定为可回收对象。不会产生循环引用问题。
核心 GC Root 类型
- 线程Thread对象。
- 系统类加载器加载的java.lang.Class对象。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 本地方法调用时使用的全局对象
二、五种对象引用
在java中,对象引用主要分为五种类型:强引用、软引用、
弱引用、虚引用、终结期引用。
1. 强引用
最常见的引用类型,默认创建的引用都是强引用。
java
Object obj = new Object(); // 强引用
特点:
- 只要强引用存在,对象永远不会被回收
- 内存不足时抛出
OutOfMemoryError
- 使用场景:普通对象创建
回收条件:
java
obj = null; // 显式解除引用
2.软引用
java
// 创建软引用
SoftReference<byte[]> softRef = new SoftReference<>(new byte[10*1024*1024]); // 10MB
特点:
- 内存充足时不会被回收
- 内存不足时可能被回收
- 使用场景:图片缓存、网页缓存
3.弱引用
java
// 创建弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 获取对象
Object obj = weakRef.get();
if (obj != null) {
System.out.println("弱引用对象存在");
}
// 强制垃圾回收
System.gc();
// 再次检查
if (weakRef.get() == null) {
System.out.println("对象已被回收");
}
特点:
- 下一次垃圾回收时立即回收(无其他引用)
- 不会阻止垃圾回收
- 使用场景:临时对象映射、监控对象状态
虚引用和终结器引用在常规开发中是不会使用的。
4.虚引用
虚引用也被称为幽灵引用或者幻影引用,它不会影响对象的生命周期,也无法通过虚引用获取到对象实例。虚引用的主要作用是在对象被垃圾回收时收到一个系统通知。创建虚引用时必须关联一个引用队列(ReferenceQueue
),当虚引用引用的对象被垃圾回收后,该虚引用会被加入到关联的引用队列中。
使用场景
虚引用主要用于在对象被回收时执行一些额外的清理操作,比如管理直接内存。直接内存不受 Java 堆内存管理,需要手动释放,使用虚引用可以在对象被回收时释放对应的直接内存资源。
示例代码
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
public static void main(String[] args) {
// 创建一个对象
Object obj = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建虚引用,关联对象和引用队列
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
// 断开强引用
obj = null;
// 建议 JVM 进行垃圾回收
System.gc();
try {
// 等待一段时间,让垃圾回收有机会执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查引用队列中是否有虚引用
if (referenceQueue.poll() != null) {
System.out.println("对象已被垃圾回收,虚引用已加入引用队列");
} else {
System.out.println("对象尚未被垃圾回收");
}
}
}
特点:
- 无法通过
get()
获取对象 - 对象被回收时收到通知
- 使用场景:直接内存清理、资源释放
5.终结器引用
java
class ResourceHolder {
private byte[] data = new byte[1024*1024]; // 1MB
@Override
protected void finalize() throws Throwable {
try {
System.out.println("执行 finalize() 清理资源");
} finally {
super.finalize();
}
}
}
// 使用
new ResourceHolder();
System.gc();
特点:
- 对象被回收前会调用
finalize()
方法 - 执行时间不确定
- 使用场景:传统资源清理(已被
try-with-resources
取代)
try-with-resources
java
try (
InputStream in = new FileInputStream("input.txt");
OutputStream out = new FileOutputStream("output.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} // 自动关闭 in 和 out
三、垃圾回收算法
1.标记清除算法
工作原理
标记清除算法分为两个阶段:标记阶段和清除阶段。
标记阶段 :从 GC Roots 开始遍历所有可达对象,将这些存活的对象标记出来。
清除阶段:遍历整个堆内存,将未被标记的对象(即垃圾对象)所占用的内存空间释放。
- 优点:实现简单,不需要额外的内存空间。
- 缺点:会产生大量内存碎片。随着时间推移,内存碎片可能导致无法分配足够大的连续内存空间给新对象,即使堆中总的可用内存足够。
2.复制算法
工作原理
复制算法(Copying)将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 优点:实现简单,运行高效,不会产生内存碎片,因为每次复制后都是连续的内存空间。
- 缺点:会浪费一半的内存空间,因为总有一块内存区域处于闲置状态。
3.标记整理算法
工作原理
标记整理算法结合了标记清除算法和复制算法的优点。同样分为两个阶段:
- 标记阶段:和标记清除算法的标记阶段相同,从 GC Roots 开始遍历,标记出所有存活的对象。
- 整理阶段:将所有存活的对象向内存的一端移动,然后直接清理掉端边界以外的内存。
优缺点
- 优点:避免了内存碎片问题,同时也不需要像复制算法那样浪费一半的内存空间。
- 缺点:需要移动对象,这会带来一定的性能开销,尤其是在对象较多时。
4.分代GC
分代GC的核心思想是根据对象的存活时间将堆内存划分成不同区域(代),不同代采用不同的垃圾回收算法。大部分的对象存活时间都很短,少部分的对象存活时间较长,基于这个特性,将堆内存划分为新生代和老年代。
新生代 :新创建的对象会被分配在新生代,新生代又细分为一个Eden区和两个survivor
区(一般称为 Survivor0
和 Survivor1
或 From
和 To
)。
老年代:经历多次垃圾回收仍存活的对象会被移动到老年代。
回收算法与回收频率
新生代
- 回收算法 :复制算法。垃圾回收时,会把
Eden
区和Survivor
区中幸存的对象复制到另一个Servivor
区,复制后Eden
区和Survivor
区(From
)被清空。 - 回收频率 :新生代垃圾回收(
Minor GC
)的频率较高,因为新对象不断创建,Eden
区内存增速很快。
老年代
- 回收算法:标记清除算法或标记整理算法。
- 回收频率 :老年代垃圾回收(
Full GC
)的频率低,因为老年代中的对象相对稳定,当Full GC
的耗时通常比Minor GC
长。
分代GC流程
1.对象创建:新创建的对象首先被分配到新生代的 Eden 区。
2.Minor GC:当 Eden 区空间不足时,触发 Minor GC。将 Eden 区和一个 Survivor 区中存活的对象复制到另一个 Survivor 区,对象每经历一次 Minor GC 且存活,其年龄计数器加 1。
3.对象晋升:当对象年龄达到一定阈值(默认 15),会被晋升到老年代。除了年龄达到阈值外,还有其他情况会导致对象晋升到老年代,如 Survivor 区空间不足时,存活对象会直接晋升到老年代。
4.Full GC:当老年代空间不足时,触发 Full GC。对整个堆进行垃圾回收,包括新生代和老年代。通常会先进行 Minor GC,再对老年代进行垃圾回收。
四、垃圾回收器
在 Java 垃圾回收中,基于分代假说 (大部分对象生命周期短,只有少数对象存活时间长),堆内存通常被划分为新生代 和老年代。不同的垃圾回收器负责回收不同代的内存,并且经常需要配合使用。
1.Serial
-
新生代垃圾回收器
-
复制算法
-
特点 :单线程工作。进行垃圾回收器时,会暂停所有的用户线程。
2.Serial Old
- 老年代垃圾回收器
- 标记整理算法
- 特点 :Serial 收集器的老年代版本,同样是单线程 工作,进行垃圾回收时,暂停所有的用户线程。
- 作为 CMS 收集器在发生"Concurrent Mode Failure"时的后备预案(当 CMS 无法及时回收老年代空间时,会退回到 Serial Old 进行 Full GC)。
3.ParNew
-
新生代垃圾回收器
-
复制算法
-
特点:Serial的多线程版本。垃圾回收时,多线程执行,但同样会阻塞所有的用户线程。
4.CMS
-
老年代垃圾回收器
-
标记清除法
-
特点:以获取最短的STW时间为目标,尽可能使用户线程和垃圾回收线程并发工作
-
初始标记
标记GC Root直接关联的对象,对象相对较少,速度很快。
-
并发标记
从初始标记的对象出发,标记出所有存活的对象,与用户线程并发执行。
-
重新标记
修正并发标记时因用户线程继续运行而导致的标记的变化。
-
并发清理
与用户线程并发执行,清理未被标记的对象。
-
缺点:
- CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。 这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N 参数(默认0)调整N次Full GC之 后再整理。
- 无法处理在并发清理过程中产生的"浮动垃圾",不能做到完全的垃圾回收。
- 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
5.Parallel Scavenge
- 新生代垃圾回收器
- 复制算法
- 特点 :
- 多线程回收:采用多线程并行的方式进行垃圾回收,在多核 CPU 环境下,能充分利用 CPU 资源,提升垃圾回收效率。
- 关注吞吐量:以达到可控制的吞吐量为目标。吞吐量指的是在应用运行期间,CPU 用于执行用户代码的时间与 CPU 总消耗时间的比值,即 吞吐量 = 用户代码执行时间 / (用户代码执行时间 + 垃圾回收时间)。高吞吐量意味着能高效利用 CPU 资源,尽快完成程序的运算任务。
- 自动调节策略 :具备自适应调节策略,通过
-XX:+UseAdaptiveSizePolicy
参数开启(默认开启)。JVM 会根据系统运行情况自动调整新生代大小 、Eden
区和Survivor
区的比例 、晋升老年代对象的年龄等参数,以达到预设的吞吐量目标。
-XX:ParallelGCThreads
:设置并行垃圾回收的线程数。一般默认值会根据 CPU 核心数动态调整,可根据实际情况手动设置,以达到更好的性能。-XX:MaxGCPauseMillis
:设置垃圾回收的最大停顿时间(毫秒),JVM 会尽力保证垃圾回收停顿时间不超过该值,但可能会影响吞吐量。-XX:GCTimeRatio
:设置吞吐量大小,取值范围是 1 - 99,默认值为 99,表示垃圾回收时间占总时间的比例不超过1 / (1 + 99) = 1%
。
6.Parallel Old
- 老年代回收器
- 标记整理法
- 工作机制:
- 标记阶段:多线程,标记存活对象,STW暂停所有用户线程。
- 整理阶段:多线程,经存活对象移向一端,并清理端边界外的内存,暂停所有用户线程。
- 高吞吐量:多线程并行回收机制,能充分利用多核 CPU 资源,减少垃圾回收的时间开销,提高应用的整体吞吐量。
- 避免内存碎片:标记 - 整理算法避免了内存碎片的产生,为后续大对象分配提供连续的内存空间,降低因内存碎片导致的 Full GC 频率。
由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合使用。

垃圾回收器组合
Java 8及以前
(1)Serial + Serial Old
- 年轻代:Serial(串行收集器,单线程STW)
- 老年代:Serial Old(串行标记-整理算法)
- 特点 :适用于客户端模式 或小内存应用(如嵌入式设备),回收时暂停所有用户线程(STW)。
- 启用参数 :
-XX:+UseSerialGC
(2)ParNew + CMS
- 年轻代:ParNew(多线程并行回收,STW)
- 老年代:CMS(Concurrent Mark-Sweep,并发标记-清除)
- 特点 :适用于低延迟Web应用 (如响应时间敏感的服务),以减少停顿时间为目的,但会产生内存碎片。
- 启用参数 :
-XX:+UseConcMarkSweepGC
(3)Parallel Scavenge + Parallel Old
-
年轻代:Parallel Scavenge(并行回收,吞吐量优先)
-
老年代:Parallel Old(并行标记-整理)
-
特点 :适用于后台计算型应用 (如大数据批处理),以最大化吞吐量为目标,允许较长的STW。
-
启用参数 :
-XX:+UseParallelOldGC
Java 9及以后
G1
设计目标
- 高吞吐量与低延迟的平衡:G1 旨在同时满足高吞吐量(Parallel Scavenge+Parallel Old)和低延迟(ParNew+CMS)的需求,它能在保证一定吞吐量的前提下,将垃圾回收的停顿时间控制在可接受的范围内。
- 可预测的停顿时间:允许用户指定在一个长度为 M 毫秒的时间片段内,垃圾回收的停顿时间不得超过 N 毫秒。
- 引入版本: JDK 7 ,JDK 9 成为默认 GC。
核心思想与架构
-
Region分区:G1的堆会被划分成多个大小相等的区域(Region)。不在物理划分新生代与老年代。
-
逻辑分区c.0:每个Region被标记为Eden、Survivor、Old、Humongous(存放大于Region一半大小的超大对象)。分代是逻辑上的概念,每个Region可以扮演不同代的角色。

- Garbage-First (回收价值优先):
- 并发全局标记: 使用 SATB算法进行并发标记,找出堆中哪些 Region 包含最多的可回收空间(垃圾)和存活对象最少的 Region。
- 回收选择: 根据用户设置的停顿时间目标,优先选择回收价值最高(即垃圾最多)的 Region 集合进行回收。这就是"Garbage-First"名称的由来。
- 疏散(Evacuation) / 复制: 将选中的 Region 中的存活对象复制到新的、空的 Region 中(复制过程需要 STW)。原 Region 被清空并加入空闲列表。这个过程同时完成年轻代回收(Minor GC)和部分老年代回收(Mixed GC)。
主要阶段:
一、Young-Only阶段(新对象分配与年轻代回收)
-
对象分配:
- 新对象分配在Eden Region中。
- 。当G1判断年轻代区不足(max默认60%),触发一次 Young GC(Minor GC)。
-
Young GC(STW事件):
-
根扫描:从GC Roots(栈、寄存器、全局变量等)开始扫描,标记直接可达对象。
-
对象复制:
- 标记 Eden Region 和 Survivor Region中存活的对象。
- 根据配置的最大暂停时间,将某些区域的存活对象复制到新的Survivor Region 中(年龄+1),达到晋升阈值(
-XX:MaxTenuringThreshold
)的对象复制到 Old Region(老年代)。 - 清空被复制的Region,加入空闲列表。
-
根据最大暂停时间和记录的平均耗时计算最大回收区域。
比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region
-
🔁 循环触发:只要应用持续分 配对象,Young GC会不断发生。
二、触发并发标记周期
- 当老年代占用率 达到阈值(
-XX:InitiatingHeapOccupancyPercent
,默认45%)时,G1 会先执行一次并发标记周期。并发标记周期结束后,若满足一定条件,就会触发 Mixed GC。
三、并发标记周期
此阶段为 Mixed GC 做准备,包含以下子阶段
- 初始标记 :
- 依附于一次Young GC进行(复用其根扫描结果)(或看作同时发生)。
- 标记所有直接从GC Roots可达的对象(速度快,停顿短)。
- **根区域扫描:
- 扫描Survivor Region(根区域)中指向老年代对象的引用。
- 与用户线程并发执行。
- 并发标记 :
- 与用户线程并发执行。
- 遍历整个堆,标记所有可达对象(使用SATB算法确保标记一致性)。
- 最终标记 :
- 修正并发标记阶段因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。
- 需要STW,采用SATB算法。
- 清除阶段 :
- STW部分 :
- 统计每个Region中存活对象比例。
- 选择回收价值最高的Region加入 CSet(Collection Set)。
- 回收完全空闲的Region。
- 并发部分:重置内部数据结构,为混合GC做准备。
- STW部分 :
✅ 标记完成:此时G1已精确知道每个Region的垃圾比例,为Mixed GC提供依据。
四、混合回收 - Mixed GC
- 目标:回收高垃圾比例的老年代Region + 所有年轻代Region(Eden+Survivor)。
- 执行方式 :
- 选择回收集合(Collection Set,简称 CSet)
- G1 会根据用户设置的最大停顿时间(
-XX:MaxGCPauseMillis
),结合各个 Region 的回收收益,挑选出新生代的 Eden Region、Survivor Region 以及部分老年代 Region 组成 CSet。
- G1 会根据用户设置的最大停顿时间(
- 复制对象(Evacuation)
- 作用:将 CSet 中存活的对象复制到新的空闲 Region 中。新生代的对象会复制到 Survivor Region 或老年代 Region,老年代的对象也会复制到其他空闲的老年代 Region 中。
- 特点:需要 STW,采用复制算法,复制完成后,原来的 CSet 中的 Region 会被释放,标记为空闲,可用于后续对象分配,且不会产生内存碎片。
- 更新引用(Reference Update)
- 在对象复制完成后,需要更新堆中其他对象对这些被复制对象的引用,确保引用指向新的对象地址。
- 选择回收集合(Collection Set,简称 CSet)
- 完成空间回收 :当老年代空间释放到安全水平后,G1重新回到 Young-Only阶段。
五、Full GC
当G1无法满足回收需求时会触发(STW时间长):
- 并发标记失败:如对象分配过快,标记速度跟不上。
- 晋升失败:Young GC时Survivor/Old区空间不足。
- 大对象分配失败:无连续Humongous Region可用。
- 处理方式 :退化到单线程的 Serial Old GC(标记-整理算法),进行全堆回收。
