记得以前有这样一副动图,用来嘲笑 JVM 的垃圾回收机制,大致的意思就是,JVM 的垃圾回收机制很工业化,但是好像是在做无用功,垃圾回收不彻底(😂)。
C/C++ 虽然需要手动释放内存,但开发者信誓旦旦,认为自己一定能清理得很彻底。那这次,我们就从头到尾来详细地聊一聊 JVM 的垃圾回收机制,看看到底如何。
垃圾回收的概念
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放空间,既要写构造函数,又要写析构函数。
构造函数和 Java 中的构造方法类似,用来创建对象,析构函数和 Java 中的 finalize 方法有一点类似,可以在对象被垃圾回收器回收之前执行清理操作,但不推荐,因为 finalize 的执行时机并不确定。
于是,有人就提出,能不能写一段程序实现这块功能,每次创建对象、释放内存空间的时候复用这段代码?
牛人还是多啊,1960 年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理 C 语言等不停的析构操作,Java 的垃圾回收机制算是发扬光大了。
Lisp 是一种函数式编程语言,我从官网上截幅图大家感受下。
垃圾判断算法
既然 JVM 要做垃圾回收,就要搞清楚什么是垃圾,什么不是垃圾。通常会有这么几种算法来确定一个对象是否是垃圾,这块也是面试当中常考的一个知识点,大家一定要掌握。
- 引用计数算法
- 可达性分析算法
- 分代收集算法
- 复制算法
引用计数算法
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。
如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。
String s = new String("沉默王二");
我们来创建一个字符串,这时候"沉默王二"有一个引用,就是 s。此时 Reference Count 为 1。
然后将 s 设置为 null。
s = null;
这时候"沉默王二"的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了。
引用计数算法将垃圾回收分摊到整个应用程序的运行当中,而不是集中在垃圾收集时。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制(随后我们会细讲)。
引用计数算法看似很美好,但实际上它存在一个很大的问题,那就是无法解决循环依赖的问题。来看下面的代码。
public class ReferenceCountingGC {
public Object instance; // 对象属性,用于存储对另一个 ReferenceCountingGC 对象的引用
public ReferenceCountingGC(String name) {
// 构造方法
}
public static void testGC() {
// 创建两个 ReferenceCountingGC 对象
ReferenceCountingGC a = new ReferenceCountingGC("沉默王二");
ReferenceCountingGC b = new ReferenceCountingGC("沉默王三");
// 使 a 和 b 相互引用
a.instance = b;
b.instance = a;
// 将 a 和 b 设置为 null
a = null;
b = null;
// 这个位置是垃圾回收的触发点
}
}
代码中创建了两个 ReferenceCountingGC 对象 a 和 b。
然后使它们相互引用。接着,将这两个对象的引用设置为 null,理论上它们会在接下来被垃圾回收器回收。但由于它们相互引用着对方,导致它们的引用计数永远都不会为 0,通过引用计数算法,也就永远无法通知 GC 收集器回收它们。
可达性分析算法
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,然后向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 之间没有任何引用相连时,即从 GC Roots 到该对象节点不可达,则证明该对象是需要垃圾收集的。
通过可达性算法,成功解决了引用计数无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。
在 Java 语言中,可作为 GC Root 的对象包括以下 4 种:
- 虚拟机栈中引用的对象
- 本地方法栈引用的对象
- 类静态变量引用的对象
- 常量引用的对象
真诚点赞 诚不我欺
回复