JVM 垃圾回收机制(GC)
文章目录
- [JVM 垃圾回收机制(GC)](#JVM 垃圾回收机制(GC))
- 观前须知
- [1. 垃圾回收机制(GC)的功能](#1. 垃圾回收机制(GC)的功能)
-
- [1.1 大杂谈](#1.1 大杂谈)
-
- [为什么 C++ 还是没有引入垃圾回收机制?](#为什么 C++ 还是没有引入垃圾回收机制?)
- STW问题
- Rust:既不用操心内存泄露,又不影响性能
- 各语言,程序运行效率
- [为什么 Python 广泛应用于 AI领域?](#为什么 Python 广泛应用于 AI领域?)
- [1.2 垃圾回收的是哪个区域?](#1.2 垃圾回收的是哪个区域?)
- [2. 垃圾回收的工作过程](#2. 垃圾回收的工作过程)
- [3. 第一步:找到垃圾](#3. 第一步:找到垃圾)
-
- [3.1 引用计数](#3.1 引用计数)
-
- 缺陷一:消耗的内存更多
- [缺陷二:可能出现 循环引用 的问题](#缺陷二:可能出现 循环引用 的问题)
- 总结:
- [3.2 可达性分析](#3.2 可达性分析)
- 总结
- [4. 第二步:释放垃圾](#4. 第二步:释放垃圾)
- [5. 垃圾收集器](#5. 垃圾收集器)
- [5. 总结](#5. 总结)
观前须知
如果你是第一次点击这篇博客的,你需要先看我的这篇博客:
JVM初识 & JVM 内存区域划分
,再过来看这篇博客。
垃圾回收机制(Garbage Collection, 简称GC),如今并不是 Java 才有的,很多语言都有这个机制。
但是,GC 的确是 Java 带头引起了潮流。
下面的博客,我们会介绍 垃圾回收机制,至于它的叫法,我有时会使用 GC 来称呼,有时用 垃圾回收 来称呼,有时候就是全称了,你知道它们都表示 垃圾回收机制 就可以了。
1. 垃圾回收机制(GC)的功能
垃圾回收机制(Garbage Collection, 简称GC)是自动管理内存的一种技术,主要用于识别和回收不再使用的对象,从而释放内存空间,避免内存泄漏 。
垃圾回收机制(Garbage Collection, 简称GC),是 Java 中,释放内存的手段。
学过 C语言/C++ ,我们知道,申请内存之后,需要手动调用 free方法 ,释放内存,否则,就会出现内存泄露 。
free方法 ,程序员不一定会时时刻刻记住这件事 ,总会不小心忘记了,或者写的代码,没有执行到 free方法 。
例如:

所以,手动释放内存太麻烦,太容易出错 ,Java 引入垃圾回收机制,进行自动释放内存 。
JVM 会自动识别出,哪个内存,是不是后续不再使用了,如果不适用了,就会自动释放不再使用的内存,避免了内存泄露。
Java垃圾回收 ,相当于开车时的 自动挡 。
C++没有垃圾回收 ,相当于开车时的 手动挡 。
开过车的都知道,开过自动挡,就不想再开手动挡了,至少大部分人是这样的。
1.1 大杂谈
由于 垃圾回收,很方便,Java 之后的各种编程语言,都引入了 垃圾回收机制。
Python,PHP,Rust,Go......等等,都引入了垃圾回收。
那么,问题来了,为什么 C++ 还是没有引入垃圾回收机制?
为什么 C++ 还是没有引入垃圾回收机制?
C语言,已经躺平了,引入 GC,不太可能了。
但是,C++还是在积极的进化当中。
不引入 GC 的原因,就是:GC,会使 C++ 程序运行的效率降低。C++ 这边,不愿承担这样的代价。
GC ,由于它会每隔一段时间,就会扫描一次程序,判断是否有 需要回收的内存,会导致程序的运行效率降低。
C++的设计目标,有两个:
- 和 C语言 兼容
- 极致的性能
GC,不符合第二个目标,所以 C++,就没有引入 GC 了。
C++ 既然没有为了解决 内存泄露 问题,引入 GC,但是,C++ 为了解决 内存泄露 问题,也是做出了策略的,引入了一个机制,叫做 智能指针,它能够一定程度缓解 内存泄露 的问题。
但是,和 GC 比起来,易用性上,简直就是弟弟中的弟弟。
STW问题
GC中,还有一个臭名昭著的问题,叫做:STW问题
STW(stop the world)问题 :触发了大规模的 GC ,可能就会因为 GC ,使得其他业务代码不得不暂停下来,等待 GC 结束,再继续执行后续代码。
这样的场景,在生活中,有一个很典型的例子:
你放假在家,用电脑打游戏,此时,你妈妈拿着扫把进来了,她会让你起来,空出那个位置,让她打扫垃圾(大规模的 GC),你就不得不起身,双手离开键盘,停止玩游戏,腾出地方(暂停业务代码)。
如果你不起来,那么你妈妈的扫把,就不是扫地了,而是打你了。
GC 发展多年,这个 STW问题,也改进了很多。
在 Java17(JDK17)及以上版本,可以做到 STW大部分情况下,占用的时间 <1ms
Rust:既不用操心内存泄露,又不影响性能
既然 GC,会消耗很多的性能,是否有办法,可以让程序员,既不用操心 内存泄露,又不影响程序性能呢?
答:新的语言 -> Rust
Rust 通过严格的编译器检查,来对代码中的内存问题,进行校验 。
编译过程中进行检查,发现问题,直接编译报错。
对于程序运行,没有任何性能上的影响。
上述,看似完美,但是,有得就有舍,天下没有白吃的午餐。
完美的解决方案,换来的代价就是:Rust 的语法,特别的复杂,比 C++ 都复杂。
各语言,程序运行效率
由于 Java 有 GC机制,论性能,确实不如 C++,但是 Java 这么多年发展下来,性能也没比 C++ 差多少。
比如,运行同一段程序:
用 C++实现,是 1 个单位时间
用 Java实现,是 1.5~2 个单位时间
用 Go实现,是 4~5 个单位时间
用 Python实现,是 100 个单位时间
看到运行效率,你会奇怪,那为什么现在流行一种说法: Go将要取代 Java?
Go:由 Google 设计,语法比较简单 ,旨在应对多核处理器 和网络服务的时代需求,其哲学是 "简单" 和 "高效"。总结来说:轻量化
Java :通过 JVM 实现跨平台能力,一次编译,到处运行,拥有庞大且成熟的生态系统,涵盖了各种开发工具、库和框架等。但是,它不够轻量化,比较笨重。
那为什么 Python,网上都在吹?
主要原因就是:Python 开发效率高。
为什么 Python 广泛应用于 AI领域?
AI 领域,真正的扛把子,是 C++ 。
AI 领域 ,它的核心逻辑,密集计算的逻辑,都是 C++ 来完成的 ,甚至是 C++ 调用 GPU(显卡)来完成的。
核心代码,是 C++ 代码实现的。
Python 只不过是调用 C++ 写的动态库而已。
但是,不是只有 Python 能调用 C++ 写的动态库,Java也能。
只不过 Python 调用 C++ 写的动态库,特别简单。
所以,AI 的兴起,是 C++ 带着Python 起飞。
1.2 垃圾回收的是哪个区域?
在这篇博客当中,我们了解到,JVM 划分出了四个核心区域:
- 程序计数器
- 元数据区
- 栈
- 堆
垃圾回收机制 ,回收的是哪一块内存?
答:堆 上的内存。
程序计数器:线程销毁,自然就销毁了。
栈:方法执行结束,栈帧的结束了,随之释放了。
元数据区:保存类对象,一般不会释放。
堆:存放着 new出来的对象,这些对象,有新对象的产生,也有旧对象的消亡,需要进行回收。
垃圾回收,回收的是 "对象"。
2. 垃圾回收的工作过程
垃圾回收的工作过程,分为两个大步骤:
- 找到 垃圾(不再使用的对象)
- 释放 垃圾(对应的内存释放掉)
接下来,我们对这两个步骤,分别进行介绍。
3. 第一步:找到垃圾
我们有两种方案,来找到垃圾:
- 引入计数(Python,PHP 采用的方案)
- 可达性分析(Java 采用了这个方案)
接下来,我们分别介绍这两种方案。
3.1 引用计数
每个对象,在 new 的时候,都搭配一个小的内存空间,保存一个整数 。

引用计数 :这个整数 ,就表示当前对象(上图为 Test对象),有多少个引用指向它 (上图,t2,t3 指向 Test对象,所以引用计数为 2)
每次进行引用赋值的时候,就会自动触发引用计数的修改。
通过用引用计数,记录有多少个 引用 指向这个对象(如上图的 Test对象)。
在 Java 当中,要想使用某个对象,一定时通过 "引用" 来完成的 (这部分是 Java基础语法 的知识)。
引用计数,有两种情况:
第一种:引用计数不为 0 ,就说明 还有引用 指向这个对象 ,这个对象,还需要进行使用,不是垃圾 。
第二种:引用计数为 0 ,说明 没有引用 指向这个对象 了,这个对象,不会进行使用了,这个对象就是垃圾。
但是,这种找垃圾的方案,有两种缺陷:
- 消耗内存多
- 循环引用问题
缺陷一:消耗的内存更多
我们需要开辟空间,来记录对象,引用的个数 。
一个对象,开辟一个空间,两个对象,就需要开辟两个空间。
如果对象本身,是比较小的,引用计数消耗的空间的比例就更大了。
假设,一个对象本身的大小是 8 个字节(8B) ,引用计数的空间是 4 个字节(4B) ,那么,对象 + 引用计数 消耗的空间,就达到 12 个字节(12B) 。
这个对象的引用计数,就相当于提高了 50% 的空间占用率。
一个对象,就提高了 50% 的空间占用率 ,如果是多个这样的对象,消耗的内存空间就更多了。
50%,是一个很高的数字了,想象一下,一个公司,每个员工,都在原工资的基础上,涨薪 50% ,这是非常恐怖的一件事。
缺陷二:可能出现 循环引用 的问题
什么叫做循环引用?
我们通过一段代码来理解:


上述过程,就是一个 循环引用 的构成过程。
此时,这两个 Test 的引用计数,都不为 0 。
虽然不为 0,但是这两个对象,都是无法使用的。
你想要访问第二个对象,就需要访问第一个对象,拿到里面的引用变量,去访问第二个对象。
要想访问第一个对象,就需要访问第二个对象,拿到里面的引用变量,去访问第一个对象。
两个对象,相互依赖,就构成了 循环引用 的现象。
这段话,你画个图,就发现,他们构成一个圆圈,循环了。
就类似于:
家里的钥匙,锁在车里了,需要打开车 。
打开车的车钥匙,又锁家了,需要家里的钥匙,才能开门 。
家里的钥匙,锁在车里了,需要打开车。
......

总结:
正是上述两点缺陷,Java 并没有采取这种方案,去找 垃圾。
Python,PHP 虽然使用引用计数,但是,需要搭配其他的方案,辅助解决上述的引用计数的 循环引用 问题。
3.2 可达性分析
引用计数方案,由于缺陷一,是有 空间开销 的 。
可达性分析 ,用 时间开销,换空间开销。
可达性分析的具体过程,是这样的:
- 以代码中的一些特定对象,作为遍历的 "起点" (这个起点叫做 GCRoots)
这些特定对象,有三种:
- 栈上的局部变量(需是 引用类型 的局部变量)
- 常量池引用指向的对象
- 静态成员(引用类型)
这三种特定的对象,程序运行到任何一个时刻,JVM 都是很容易可以获取到的。
- GC会尽可能的进行遍历程序中的对象,判定某个对象,是否能访问到
- 每次访问到一个对象,这个对象,就会被标记成 "可达" ,当完成所有的对象的遍历 后,未被标记成 "可达" 的对象,就会被标记为 "不可达"。
- 通过可达性分析 ,
"可达" 的对象 ,是要被使用 的,不会进行回收
"不可达" 的对象 ,是不会被使用 的,就是要回收的垃圾
上述过程,一个程序中,有多少个对象,JVM自身是知道的。
我们用一段代码,来演示说明一下:
java
class Node {
String val;
Node left;
Node right;
}
Node build() {
Node a = new Node("a");
Node b = new Node("b");
Node c = new Node("c");
Node d = new Node("d");
Node e = new Node("e");
Node f = new Node("f");
Node g = new Node("g");
a.left = b;
a.right = c;
b.left = d;
b.right = e;
e.left = g;
c.right = f;
return a;
}
这段代码,是用来构造一棵 二叉树 的代码。
调用 build()方法 构造的二叉树,长这样:

可达性分析,用流程图来表示,是这样的:

找到一个节点,就会把那个节点对象,标记为 "可达"。
可达性分析的过程,很像 二叉树 的遍历过程。
此时,我运行一条这样的代码:root.right.right = null
也就是把 c 指向 的对象,改变了,指向为 null;

当 GC 机制,再次进行 可达性分析 的过程中,f 这个节点对象,就会被标记为 "不可达"对象 。
那么,f 节点对象,就会被当作 垃圾,被 GC 回收。
可达性分析,这个过程,是一个周期性的过程。
每隔一定的时间,就会触发一次这样的 可达性分析 的遍历,寻找 垃圾。
就像我们在初中高中,每一天都有同学负责打扫教室,这是周期性的。
正是因为 GC,它会周期性的遍历一遍所有的对象,C++ 才会嫌弃 GC机制 。
如果 对象 特别多,这个过程就得非常消耗时间和资源,降低了 程序运行 的效率。
总结
垃圾回收,找垃圾的方案,我们介绍了两个:
- 引用计数
- 可达性分析
知道这两个方案的工作过程,面试中能够说出来,就可以了。
至于面试中怎么回答,回答哪一个,要看面试官怎么问的。
问题:介绍一下 垃圾回收机制 的基本策略 ,
回答方式:两个方案都可以回答(引用计数,可达性分析)
问题:介绍一下 Java的垃圾回收 的基本策略
回答方式:只介绍 可达性分析 找垃圾的过程
4. 第二步:释放垃圾
这部分的内容,有 3000字+,如果放在这篇博客,内容太多,影响观看体验。
我将这部分内容,放到这篇博客中:JVM 垃圾回收机制(GC)-- 释放垃圾
你需要点击链接,观看这篇博客。
当然,你可以看到这里后,休息,之后再点击博客链接,继续学习。
5. 垃圾收集器
JVM 中,存在 垃圾收集器 模块,用来实现上述 分代回收 的策略。
分代回收,只是最基本的思想,落实到具体的垃圾收集器上,会有一些特定的,更进阶的策略,去实现 分代回收 的思想。
至于垃圾收集器,我这里就不展开讲了,主要面试也不考,大家想要了解的,可以去 B站 搜索相关视频进行学习。
5. 总结
这篇博客,我们主要学习了 JVM的垃圾回收机制(GC)。
垃圾回收机制(GC),是 Java 中,释放内存的手段。
垃圾回收的工作过程,分为两个大步骤:
- 找到 垃圾(不再使用的对象)
- 释放 垃圾(对应的内存释放掉)
我们有两种方案,来找到垃圾:
- 引入计数(Python,PHP 采用的方案)
- 可达性分析(Java 采用了这个方案,重点了解)
释放垃圾,我们有四种方案:
- 标记-清除
- 复制算法
- 标记-整理
- 分代回收 (Java 采用的方案,重点了解)
最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!