一、什么是 JVM 垃圾回收?
首先,我们思考一个问题:
在 Java 中,我们创建对象时,通常不需要手动释放内存。
例如:
java
public class Main {
public static void main(String[] args) {
User user = new User();
user = new User();
user = null;
}
}
这里发生了什么?
-
new User():创建第一个 User 对象。 -
user = new User():创建第二个 User 对象,第一个对象不再被变量 user 引用。 -
user = null:第二个对象也不再被 user 引用。
那么问题来了:
这两个 User 对象占用的内存什么时候释放?
这就是 JVM 垃圾回收机制要解决的问题。
简单来说:
JVM 垃圾回收(GC)就是自动识别不再使用的对象,并回收它们占用的内存空间。
它有两个核心任务:
-
找出哪些对象已经没有用了。
-
回收这些对象占用的内存。
二、JVM 如何判断一个对象是不是垃圾
这是 GC 最核心的问题。
JVM 主要涉及两种判断思路:
1. 引用计数法
原理非常简单:
给每个对象维护一个引用计数器。
每当有一个引用指向该对象,计数器就加 1。
每当有一个引用失效,计数器就减 1。
当计数器为 0 时,就认为对象可以被回收。
例如:
java
User user1 = new User();
User user2 = user1;
此时:
user1 和 user2 两个变量都指向同一个 Use r对象
两个变量都指向同一个对象,因此引用计数为 2。
但是引用计数法有一个严重问题:无法解决循环引用问题。
例如:
java
class A {
B b;
}
class B {
A a;
}
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
}
}
此时:
-
A 对象引用着 B 对象。
-
B 对象引用着 A 对象。
-
外部已经没有任何变量引用它们。
但是两个对象的引用计数都不为 0。
所以,引用计数法无法判断它们是否已经成为垃圾。
2. 可达性分析算法
这是 HotSpot JVM 判断对象是否存活的核心方法。
它的基本思想是:
从一组被称为 GC Roots 的根对象出发,向下搜索所有能够到达的对象。
能够被访问到的对象,说明仍然可能被使用。
无法被访问到的对象,就可以被判定为不可达对象。
看下面这个示意图:

常见的 GC Roots 有哪些?
1、虚拟机栈中引用的对象(例如局部变量引用的对象)。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中 JNI 引用的对象。
5、JVM 内部的一些引用对象,例如活动线程、系统类加载器等。
注意:对象不可达不代表一定会立即被回收,还需要考虑对象的终结处理等特殊情况。
三、JVM 垃圾回收主要发生在哪里?
理解 GC,必须先了解JVM 堆内存结构。
Java 对象主要分配在堆内存中,而堆通常按照对象生命周期特点进行分代管理。

传统分代模型主要分为:
|----------------------|-----------|
| 区域 | 作用 |
| 新生代 Young Generation | 存放新创建的对象 |
| 老年代 Old Generation | 存放长期存活的对象 |
新生代又可以分成:
-
Eden 区:大多数新对象首先分配在这里。
-
Survivor 0(S0):存放经过 GC 后存活的对象。
-
Survivor 1(S1):与 S0 交替使用。
为什么要这样设计?
因为 JVM 发现:
大多数 Java 对象都是朝生夕死的。
例如:
java
public void test() {
for (int i = 0; i < 10000; i++) {
User user = new User();
}
}
循环创建的很多对象,在使用结束后就没有引用了。
因此,把新生代和老年代分开,可以针对不同生命周期的对象采用不同的回收策略。
四、JVM 垃圾回收有哪些算法?
1. 标记-清除算法(Mark-Sweep)
分为两个步骤:
-
标记:找到所有需要保留的对象。
-
清除:回收没有被标记的对象。
优点:
-
实现简单。
-
不需要移动存活对象。
缺点:
-
会产生大量内存碎片。
-
分配大对象时可能找不到连续的内存空间。
2. 标记-复制算法(Mark-Copy)
核心思想:
将内存划分为两块,每次只使用其中一块。
当发生 GC 时:
-
找出存活对象。
-
将存活对象复制到另一块内存。
-
清空原来的内存区域。
优点:
-
不会产生内存碎片。
-
回收过程相对简单。
缺点:
-
需要额外的内存空间。
-
存活对象较多时,复制成本较高。
因此,这种算法很适合新生代,因为新生代对象通常存活率比较低。
3. 标记-整理算法(Mark-Compact)
核心思想:
-
标记所有存活对象。
-
将存活对象向内存一端移动。
-
清理边界之外的内存。
优点:
-
不会产生内存碎片。
-
不需要额外保留一整块复制空间。
缺点:
-
需要移动对象。
-
整理过程有一定性能开销。
这种算法常用于老年代的垃圾回收实现中。
三种算法对比
| 算法 | 优点 | 缺点 |
|---|---|---|
| 标记-清除 | 简单 | 内存碎片 |
| 标记-复制 | 无碎片,效率较高 | 需要额外空间 |
| 标记-整理 | 无碎片 | 移动对象开销 |
五、JVM 垃圾回收器有哪些?
面讲的是 GC 算法,现在讲真正执行垃圾回收工作的组件:垃圾回收器。
常见的有:
| 垃圾回收器 | 特点 |
|---|---|
| Serial | 单线程,简单 |
| Parallel Scavenge | 注重吞吐量 |
| ParNew | 新生代并行回收器 |
| CMS | 注重低停顿,采用并发标记清除 |
| G1 | 面向服务端应用,兼顾吞吐量和停顿时间 |
| ZGC | 面向低延迟,支持并发回收 |
其中有三个尤其值得学习:
1. CMS
CMS 的主要特点是尽可能减少垃圾回收时应用线程的停顿时间。
主要阶段:
-
初始标记
-
并发标记
-
重新标记
-
并发清除
2. G1
G1 将堆划分为多个大小相同的 Region,不再简单地按照连续空间划分新生代和老年代。
它可以优先回收垃圾最多、回收收益较高的 Region。
3. ZGC
ZGC 是一款低延迟垃圾回收器,主要通过并发处理和着色指针等技术减少停顿时间。
六、用一个实际案例理解 GC
假设我们运行下面的代码:
java
public class Main {
public static void main(String[] args) {
User user1 = new User();
User user2 = new User();
User user3 = new User();
user1 = null;
user2 = null;
System.gc();
}
}
假设三个对象都成功分配在堆中。
执行过程:

注意:System.gc() 只是向 JVM 提出执行 GC 的建议,并不保证立即执行垃圾回收。