第四章:垃圾收集(GC)原理 —— JVM 如何自动回收垃圾?
本章目标:
- 理解 GC 的本质
- 掌握对象何时变成垃圾
- 理解 GC Root 与可达性分析
- 掌握 JVM 常见垃圾回收算法
- 理解 Minor GC、Major GC、Full GC
- 为后续 G1、ZGC、JVM调优打下基础
一、为什么需要 GC?
如果你学过 C/C++:
User* user = new User();
delete user;
程序员必须手动释放内存。
如果忘记:
User* user = new User();
没有:
delete user;
就会产生:
Memory Leak(内存泄漏)
Java 采用:
Automatic Garbage Collection
自动垃圾回收
开发者只负责:
new User();
JVM负责:
发现垃圾
回收垃圾
整理内存
二、什么是垃圾?
很多人误以为:
User user = new User();
对象创建后就是垃圾。
显然不是。
垃圾的定义:
无法再被程序访问的对象
例如:
User user = new User();
user = null;
此时:
User对象
已经没有任何引用。
变成:
垃圾对象
等待GC回收。
三、如何判断对象是垃圾?
这是GC最核心的问题。
JVM历史上主要有两种方案:
引用计数法
可达性分析法
四、引用计数法(Reference Counting)
原理非常简单。
对象维护一个计数器:
被引用一次 +1
引用失效 -1
例如:
User user = new User();
User对象
引用数 = 1
再来:
User user2 = user;
User对象
引用数 = 2
如果:
user = null;
引用数 = 1
再执行:
user2 = null;
引用数 = 0
GC回收。
五、为什么 JVM 不采用引用计数?
因为无法解决循环引用。
例如:
class A {
B b;
}
class B {
A a;
}
创建对象:
A a = new A();
B b = new B();
a.b = b;
b.a = a;
结构:
A ---> B
↑ ↓
└─────┘
然后:
a = null;
b = null;
程序已经访问不到:
A对象
B对象
但是:
A引用B
B引用A
引用数仍然:
A = 1
B = 1
永远不会回收。
因此:
Java
Go
C#
HotSpot JVM
全部采用:
可达性分析
六、可达性分析(Reachability Analysis)
这是现代 JVM 的核心算法。
基本思想:
从一组根对象开始搜索。
如果对象能够到达:
存活
如果无法到达:
垃圾
图示:
GC Root
│
▼
ObjectA
│
▼
ObjectB
│
▼
ObjectC
全部存活。
如果:
GC Root
ObjectD
│
▼
ObjectE
与 Root 完全断开:
ObjectD
ObjectE
变成垃圾。
七、什么是 GC Root?
GC Root:
垃圾回收的起点
常见GC Root:
1、栈中的引用
public void test() {
User user = new User();
}
栈:
Stack
└── user
堆:
Heap
└── User对象
user就是GC Root链路的一部分。
2、静态变量
public class UserService {
static User user = new User();
}
静态变量属于类。
类属于方法区。
始终可达。
3、常量引用
private static final User USER =
new User();
也是GC Root。
4、本地方法引用
native method
引用的对象。
八、对象一定会立刻回收吗?
不会。
很多人误解:
user = null;
对象立即消失。
实际上:
仅仅失去引用
真正回收时机:
GC触发时
例如:
user = null;
//对象暂时还在
直到:
Minor GC
Major GC
Full GC
执行。
九、对象死亡流程
对象不是第一次发现不可达就马上回收。
第一次GC:
发现不可达
进入:
待回收区
如果对象重写:
@Override
protected void finalize() {
}
JVM会给一次机会。
例如:
@Override
protected void finalize() {
SAVE_HOOK = this;
}
对象复活。
所以:
第一次不可达
≠
立即死亡
注意:
从 JDK9 开始:
finalize()
已废弃
不要使用。
十、Java 引用类型
很多开发者只知道:
User user;
实际上 JVM 有四种引用。
十一、强引用(Strong Reference)
最常见。
User user = new User();
特点:
只要强引用存在
GC绝不会回收
即使:
OOM
也不会优先回收。
十二、软引用(Soft Reference)
SoftReference<User>
特点:
内存充足
不回收
内存不足
回收
适合:
缓存
图片缓存
十三、弱引用(Weak Reference)
WeakReference<User>
特点:
只要GC发生
立即回收
无论内存是否充足。
例如:
WeakHashMap
底层就大量使用。
十四、虚引用(Phantom Reference)
最弱引用。
PhantomReference
特点:
无法获取对象
仅用于跟踪回收
主要用于:
直接内存回收
NIO
Netty
十五、GC 核心算法
确定垃圾后:
如何回收?
就是GC算法。
主要有:
标记-清除
复制
标记-整理
分代收集
十六、标记-清除(Mark-Sweep)
第一步:
标记垃圾
第二步:
直接删除
示意:
GC前:
[ A ][ B ][ C ][ D ]
B、D死亡:
[ A ][ X ][ C ][ X ]
优点:
实现简单
缺点:
内存碎片严重
结果:
[ A ][空][ C ][空]
十七、复制算法(Copying)
年轻代核心算法。
内存分两块:
From区
To区
GC前:
From
[A][B][C][D]
B、D死亡。
GC后:
To
[A][C]
直接复制。
优点:
无碎片
速度快
缺点:
浪费一半空间
十八、标记-整理(Mark-Compact)
老年代经典算法。
GC前:
[A][X][C][X][E]
整理后:
[A][C][E]
连续排列。
优点:
没有碎片
缺点:
移动对象成本高
十九、为什么 JVM 要分代?
HotSpot发现:
98%以上对象
朝生夕死
例如:
new String();
new UserDTO();
new HashMap();
方法结束就没了。
少数对象:
缓存
连接池
Spring Bean
长期存活。
因此:
年轻代
老年代
分开管理。
二十、堆内存结构
JDK8经典结构:
Heap
┌─────────────────┐
│ Young │
├─────────────────┤
│ Old │
└─────────────────┘
年轻代:
Eden
Survivor0
Survivor1
结构:
Young
┌─────────────┐
│ Eden │
├──────┬──────┤
│ S0 │ S1 │
└──────┴──────┘
二十一、对象晋升过程
对象创建:
Eden
第一次GC:
Eden
↓
S0
第二次GC:
S0
↓
S1
年龄增加:
Age = 2
达到阈值:
15次
(默认)
进入:
Old Gen
二十二、Minor GC、Major GC、Full GC
面试必问。
Minor GC
回收:
Young Gen
特点:
频繁
速度快
STW时间短
Major GC
回收:
Old Gen
特点:
速度慢
耗时长
Full GC
回收:
Young
+
Old
+
Metaspace
特点:
最慢
影响最大
线上最怕:
频繁Full GC
二十三、什么是 STW(Stop The World)?
GC过程中:
暂停所有用户线程
例如:
用户请求
订单支付
接口调用
全部暂停。
这就是:
Stop The World
简称:
STW
GC优化目标:
减少STW时间
这也是:
CMS
G1
ZGC
Shenandoah
出现的原因。
二十四、线上最常见 GC 问题
问题1
频繁Minor GC
原因:
年轻代太小
对象创建过多
问题2
频繁Full GC
原因:
老年代不足
内存泄漏
问题3
OOM
原因:
对象无法回收
例如:
static List<User> cache =
new ArrayList<>();
无限增长。
二十五、面试高频题
为什么 JVM 不采用引用计数?
因为无法解决:
循环引用
GC Root 有哪些?
栈变量
静态变量
常量
Native引用
Minor GC 和 Full GC 区别?
Minor GC
回收年轻代
Full GC
回收整个堆
为什么年轻代采用复制算法?
因为:
对象存活率低
复制成本小。
为什么老年代采用标记整理?
因为:
对象存活率高
不能频繁复制。
本章总结
牢记下面这张图:
GC Root
│
可达性分析(Reachability)
│
┌───────────┴───────────┐
│ │
存活对象 垃圾对象
│ │
▼ ▼
保留 GC回收算法
│
┌──────────┬──────────┬──────────┐
│ │ │
标记清除 复制算法 标记整理
│
▼
分代收集思想
│
┌─────────────────────────┐
│ Young Gen → Old Gen │
└─────────────────────────┘
掌握这一章,你已经具备了理解 JVM 内存回收机制的核心基础。
下一章《JVM 垃圾收集器详解(Serial、Parallel、CMS、G1、ZGC)》将深入分析:
- 为什么 CMS 被淘汰?
- G1 为什么成为 JDK 默认 GC?
- ZGC 如何做到毫秒级停顿?
- 生产环境应该如何选择垃圾收集器?
这一章也是高级开发、架构师和 JVM 调优的核心知识。