第四章:垃圾收集(GC)原理 —— JVM 如何自动回收垃圾?

1 阅读6分钟

第四章:垃圾收集(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)

年轻代核心算法。


内存分两块:

FromTo

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 调优的核心知识。