莫那·鲁道

惯于闲看花飞花落, 心念天际云卷云舒.

Coder, 欢迎留言 😆.


GitHub

深入浅出-JVM-GC(1)

# 前言

初级 Java 程序员步入中级程序员的有一个无法绕过的阶段——GC(Garbage Collection)。作为 Java 程序员,说实话,很幸福,不用像 C 程序员那样,时刻关心着内存,就像网上有句名言——生活从来都不容易,只不过是有人替你负重前行!是的,GC 在替我们做这些脏活累活,GC 像让我们把精力都放在业务上,而不用每时每刻都在想着内存。现在,GC 也是每个语言的标准配置了。不然谁会去使用这个语言呢?

然而,作为一个合格的程序员,对底层的好奇是进步的动力,如果一个程序员失去了好奇心,那就可以说他在程序员这条道路上就结束了。

难道我们不好奇 GC 到底是怎么做的吗?接下来,我们就分析 GC 做了哪些事情。

实际上,GC 主要做3件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

说到底,GC 就是做这3件事情,如果你能解决这3个问题,那么你也可以实现一个 GC。

那我们就一个一个问题来看看。

1. 哪些内存需要回收

还记得我们之前分享的关于 JVM 运行时数据区吗?有堆,有栈,有方法区(永久代),还有直接内存,还有 PC 寄存器。其中,GC 的主要战场就是堆,当然,方法区也是需要 GC 的。但重点还是堆。

我们知道,堆中内存是共享的,基本所有的对象都是在堆中创建。当一个对象不需要使用了,理论上我们就需要释放他所占用的内存。

问题来了,如何分辨一个对象不需要使用了呢?答案是:不可能被任何途径使用的对象。也就是说他没有了任何引用。我们知道,引用在栈中,实例在堆中,当一个实例没有了指向他的引用,我们认为,这个实例就需要清除并释放他所占用的内存了。

那么 GC 是如何实现的呢?一般而言有2种方法:

  1. 引用计数法(有缺陷,无法解决循环引用问题,JVM 没有采用)
  2. 可达性分析(解决了引用计数的缺陷,被 JVM 采用)

什么是引用计数法呢?

给对象中添加一个引用计数器,每当有一个地方引用他是,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能在被使用的。

虽然乍看这个算法简单,效率也高,但有一个问题这个算法无法解决,就是循环引用。试想一下:A 对象引用了 B,B 对象也引用了 A,但 A 和 B 都不被别的地方使用,也就是说,实际上这两个对象是垃圾对象,但是由于他们互相持有引用,导致他们的引用计数器都不为0,因此系统无法判断是垃圾,也无法回收他们。

所以,在现在的 JVM 中,是没有使用这个算法的。我们知道就行。

引用计数法不行,那就再说说可达性分析算法。

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,所有所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(也就是对象不可达)时,则证明此对象是不可用的。如下图所示,obj5 , obj6, obj7 虽然互相有关联,但是他们到 GC Roots 是不可达的,所以他们将会判定为是可回收的对象。

可达性分析算法

那么哪些对象可以作为 GC Roots 对象呢?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中 JNI (即 native 方法)引用的对象。

2. 什么时候回收?

注意:即使是在可达性分析算法中不可达的对象,也并非是”非死不可的”,这时候他们实际上是处于 “缓刑” 阶段。因为要真正宣告一个对象的死亡,至少需要经历两次标记过程:

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。注意:当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。也就是说,finalize 方法只会被执行一次。

如果这个对象被判定为有必要执行 finalize 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的 Finalizer 线程去执行它。注意:如果一个对象在 finalize 方法中运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃。 ========================================================= finalize 方法是对象逃脱死亡命运的最后一道关卡。稍后 GC 将对队列中的对象进行第二次规模的标记,如果对象要在 finalize 中 “拯救” 自己,只需要将自己关联到引用上即可,通常是 this。如果这个对象关联上了引用,那么在第二次标记的时候他将被移除出 “即将回收” 的集合;如果对象这时候还没有逃脱,那基本上就是真的被回收了。

这里需要注意的一点就是:一个对象如果重写了 finalize 方法,那么这个方法最多只会被执行一次。

建议:如非必要,不要重写该方法。可以使用 try-finally 代替,此方式更好,更及时。同时注意:在 Mysql 的 JDBC 驱动中,com.mysql.jdbc.ConnectionImpl 就实现了 finalize 方法,作用是:当一个 JDBC Connection 被回收时,需要进行连接的关闭,如果开发人员忘记了关闭,则在 finalize 方法中进行关闭。但是,由于其调用的不确定性,这不能单独作为可靠的资源回收手段。

到这里,我们知道了什么时候进行回收:如果一个对象重写了 finalize 方法且这个方法没有被 JVM 调用过,那么这个对象会被放入一个队列等待被一个低优先级的线程执行 finalize 方法,如果在这个方法中对象不能自救,则这个对象在第二次标记过程中就会被标记死亡,等待 GC 回收。

3. 如何回收?

如何回收,这个问题非常的大,涉及到各种垃圾回收算法,各种垃圾收集器。限于本篇的篇幅,楼主将不会在这篇文章里深入探讨,这里只会列出一些大纲,这些大纲将是后面文章的摘要,我们将在后面的文章中深入探讨如何回收。

那么,有哪些摘要呢?

3.1 垃圾回收算法
  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)

这些算法是 GC 的基础,所有 GC 的实现都是基于这些算法来清除无用对象,然后释放内存空间。我们将会在后面的文章一个一个讲解。

3.2 有哪些垃圾收集器
  1. Serial 串行收集器(只适用于堆内存256m 一下的 JVM )
  2. ParNew 并行收集器(Serial 收集器的多线程版本)
  3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
  4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
  5. G1 收集器(JDK 9 的默认 GC)
3.3 有哪些GC
  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

好,以上就是如何回收的大纲,我们将在后面的文章中慢慢讲解。

总结

这篇文章主要总结了什么是 GC ,以及 GC 的作用,GC 主要做了3件事情,哪些内存需要回收,什么时候回收,如何回收。我们知道了 GC 通过可达性分析知道了哪些内存需要回收,那什么时候回收呢?执行 finalize 方法后如果还没有复活,将被回收。第三个问题:如何回收呢?这个问题是一个大课题,我们只是列出了一些大纲,比如有哪些垃圾收集器,有哪些垃圾算法,有哪些 GC 过程。这些细节我们将在后面慢慢讲解,逐步深入。