参考文献:周志明《深入理解Java虚拟机》第二版
因为 Java 具有自动垃圾回收机制,所以,垃圾收集(Garbage Collection,GC),是 Java 技术的核心之一,是每一个 Java 程序员必知必备的一项技术,为了深入掌握 Java 技术,我们必须学习 JVM 中的 GC 是如何实现的
GC 中包含三个主要的问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
我们对这三个问题逐一讨论
哪些内存需要回收?
首先我们应该清楚,在 Java 内存运行时区域的各个部分中,程序计数器(PC)、虚拟机栈(VM Stack)、本地方法栈(NM Stack)三个区域随线程而生,随线程而灭,这几个区域的内存分配和回收都具有确定性,不必过多考虑 GC 问题。而 Java 堆(Java Heap)和方法区(Method Area)就不一样了,因为,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行中才能知道会创建哪些对象,所以这部分内存的分配与回收是动态的,我们需要在 Java 堆 和 方法区 中研究 GC 问题
需要回收哪些内存,就是如何判定对象的“生”(正在被使用)或“死”(不会再被使用),有两种判定方法:
引用计数法
这是一种简单直接的判定方法,具体实现就是:给对象中添加一个引用计数器,每当它被引用时计数器就加 1 ;引用失效时,计数器就减 1,所以,计数器值为 0 的对象,就是已经没用的对象,可以被回收
这种方法实现简单,判定高效,但是却几乎不被 JVM 使用,原因是它有一个致命缺陷:无法解决对象相互循环引用问题,比如,有两个已经没用的对象 objA 和 objB ,它们虽已经不会再被程序引用,但是它们相互引用 objA.instance = objB; objB.instance = objA; 这样一来,它们的引用计数值都不为 0,无法被清理掉,它们就会一直占用空间
可达性分析法
这个判定对象“生死”算法是目前比较主流的 ,这个算法的基本思路是通过一系列的称为 GC Roots 的对象作为起始点,然后从这些结点向下搜索,搜索过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(或是说从 GC Roots 不可达),就证明对象不可用,可以清除了
如图,Object 6, 7, 8 都是可清除的对象
那么,GC Roots 具体都是什么呢?它包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量的引用对象
- 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象
ps. JVM规范中没有要求方法区必须有 GC,因为“性价比”低,过于复杂会影响性能,而且一些 JVM 将 Java 堆与方法区合为一体,因此,在本文中不讨论方法区的GC
什么时候回收?
因为 GC 主要发生在 Java 堆中,要回答这个问题就必须先明白 Java 堆的分区和 GC 的分类
Java 堆分为 新生代 和 老年代 两个区,其中新生代又分为 一个 Eden区和 两个 Survior区(from Survior 和 to Survior),分出两个 Survior区是因为新生代回收算法采用 复制算法(后文会讲解)
GC 分为 新生代 GC(Minor GC) 和 老年代 GC(Major GC / Full GC)
新创建的对象实例都会先进入 Eden区, 当 Eden区的空间不够新对象存入时就会触发 Minor GC,经历一次 Minor GC 而未被清除的对象会被存入 Survior区,之后每经历一次 Minor GC 未被清除,对象的“年龄”就会增长一岁,当岁数足够时,就会进入老年代,当老年代满时,就会触发一次 Full GC
因为 Java 对象大多都是朝生夕灭,所以 Minor GC 非常的频繁,一般速度也比较快,Full GC 不常发生,而且速度要比 Minor GC 慢 10 倍以上
如何回收?
这个问题是十分重要的,它就是说 GC 算法是如何实现的,GC 算法有很多,我们在此讨论最经典的几个
标记-清除算法
这是最简单基础的 GC 算法,它分为“标记”、“清除”两个阶段,就是简单的标记出可回收对象,然后将其清除,不做其他操作
这个方法虽简单,但是它有两个弊病:
① 效率不高
② 会产生大量空间碎片(如上图)
大量的空间碎片会导致大对象进入时,无法找到足够连续的内存空间,不得不触发一次 Minor GC
复制算法
它将可用内存分为大小相等的两部分,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一个上面,然后再把使用过的内存空间一次清理掉,这种算法简单高效,不会产生内存碎片,但是代价是内存容量减小了一半,但是因为它的高效快速,所以被应用在 Minor GC 中
标记-整理算法
这个算法就是在标记-清除算法的基础上加入了整理内存空间的功能,它不会产生内存碎片,也不会牺牲空间,但是因为它要逐个整理导致回收速度缓慢,所以它被应用在老年代 Full GC 中