深入浅出-JVM-GC(1)
初级 Java 程序员步入中级程序员的有一个无法绕过的阶段——GC(Garbage Collection)。作为 Java 程序员,说实话,很幸福,不用像 C 程序员那样,时刻关心着内存,就像网上有句名言——生活从来都不容易,只不过是有人替你负重前行!是的,GC 在替我们做这些脏活累活,GC 像让我们把精力都放在业务上,而不用每时每刻都在想着内存。现在,GC 也是每个语言的标准配置了。不然谁会去使用这个语言呢?
然而,作为一个合格的程序员,对底层的好奇是进步的动力,如果一个程序员失去了好奇心,那就可以说他在程序员这条道路上就结束了。
难道我们不好奇 GC 到底是怎么做的吗?接下来,我们就分析 GC 做了哪些事情。
实际上,GC 主要做3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
说到底,GC 就是做这3件事情,如果你能解决这3个问题,那么你也可以实现一个 GC。
那我们就一个一个问题来看看。
1. 哪些内存需要回收
还记得我们之前分享的关于 JVM 运行时数据区吗?有堆,有栈,有方法区(永久代),还有直接内存,还有 PC 寄存器。其中,GC 的主要战场就是堆,当然,方法区也是需要 GC 的。但重点还是堆。
我们知道,堆中内存是共享的,基本所有的对象都是在堆中创建。当一个对象不需要使用了,理论上我们就需要释放他所占用的内存。
问题来了,如何分辨一个对象不需要使用了呢?答案是:不可能被任何途径使用的对象。也就是说他没有了任何引用。我们知道,引用在栈中,实例在堆中,当一个实例没有了指向他的引用,我们认为,这个实例就需要清除并释放他所占用的内存了。
那么 GC 是如何实现的呢?一般而言有2种方法:
- 引用计数法(有缺陷,无法解决循环引用问题,JVM 没有采用)
- 可达性分析(解决了引用计数的缺陷,被 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 对象呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 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 垃圾回收算法
- 标记清除算法
- 复制算法
- 标记整理算法
- 分代收集算法(堆如何分代)
这些算法是 GC 的基础,所有 GC 的实现都是基于这些算法来清除无用对象,然后释放内存空间。我们将会在后面的文章一个一个讲解。
3.2 有哪些垃圾收集器
- Serial 串行收集器(只适用于堆内存256m 一下的 JVM )
- ParNew 并行收集器(Serial 收集器的多线程版本)
- Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
- CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
- G1 收集器(JDK 9 的默认 GC)
3.3 有哪些GC
- Young GC(又称 YGC,minor GC,年轻代 GC)
- Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
- Full GC(又称 major GC)
- Mixed GC(混合 GC,G1 收集器独有)
好,以上就是如何回收的大纲,我们将在后面的文章中慢慢讲解。
总结
这篇文章主要总结了什么是 GC ,以及 GC 的作用,GC 主要做了3件事情,哪些内存需要回收,什么时候回收,如何回收。我们知道了 GC 通过可达性分析知道了哪些内存需要回收,那什么时候回收呢?执行 finalize 方法后如果还没有复活,将被回收。第三个问题:如何回收呢?这个问题是一个大课题,我们只是列出了一些大纲,比如有哪些垃圾收集器,有哪些垃圾算法,有哪些 GC 过程。这些细节我们将在后面慢慢讲解,逐步深入。