垃圾回收机制
垃圾回收机制
一、简介
首先,我们要知道JVM中的垃圾是指什么:
在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。JVM进行垃圾回收之前,首先要判断哪些对象是垃圾(可销毁,占用内存可被回收)。
在 Java体系中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。
在JAVA体系中,垃圾回收机制指的就是对象的内存回收机制(回收目标、回收策略),这种回收是不需要程序员主动操作的,由虚拟机在后台完成。
而上面提到的"死亡"的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。
引用计数法 每个对象的对象头中都存了一个引用计数器counter,有对象引用时计数值 +1,引用被释放时计数值 -1,当计数器为 0 时就可以被JVM回收。这种方式有一个缺点是不能解决循环引用的问题。
可达性分析法 可达性分析法也被称之为GCRoot根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。 可达性分析的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

如上图所示,object1~object4对GC Root都是可达的,说明不可被回收,object5和object6对GC Root节点不可达,说明其可以被回收。
那么可以视为GC Roots的对象是哪些呢?
在Java中,可作为GC Root的对象包括以下几种:

二、垃圾回收算法
有了判断对象是否存活的标准之后,我们再来了解一下GC的相关算法。
目前总共有四种垃圾回收算法:

1、标记-清除算法
标记-清除算法是最开始时采用的垃圾回收算法,也是最简单最基础的垃圾处理算法。
原理如下:

该算法有一个最大的问题是标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
后续的收集算法都是前一个算法的问题进行改进而得到的。
2、复制算法
原理如下:

它将整个堆内存按容量划分为大小相等的两块,每次实际只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。
目前主流的商业虚拟机都采用这种收集算法来回收新生代区域内存。IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
复制算法改进了标记-清除算法的效率问题,同时带来了一个新问题: 可用内存缩小到了原先的一半。
3、标记-整理算法
复制算法主要用于回收新生代的对象,但是这个算法并不适用于老年代。因为老年代的对象存活率都较高。
因此,JVM的研发人员又提出了新的算法:标记-整理算法:

该算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法
通过前面的笔记我们知道JVM将堆划分了不同的区域:新生代、老年代、永久代,每个区域的对象生命周期是不一样的。
而分代收集算法,其实就是针对不同生命周期的对象采用不同的垃圾回收算法进行回收,以便提高回收效率。 在JVM的新生代区域内存中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清理” 或者 “标记—整理” 算法 来进行回收。
- 新生代:复制算法
- 老年代:标记-清除算法、标记-整理算法
分代收集算法,其实是不是一个新的算法,而是对已经出现的复制算法、标记整理算法、标记清除算法进行一个灵活的整合和运用。
三、垃圾收集器

垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。以串行模式工作的收集器,称为Serial Collector,即串行收集器;与之相对的是以并行模式工作的收集器,称为Paraller Collector,即并行收集器。
1、Serial 收集器
Serial 是串行收集器,有两种:Serial和Serial Old。
串行收集器采用单线程方式进行收集,且在 GC 线程工作时,系统不允许应用线程打扰。此时,应用程序进入STW暂停状态,即 Stop-the-world。Stop-the-world 暂停时间的长短,是衡量一款收集器性能高低的重要指标。Serial 是针对新生代的垃圾回收器,采用“复制”算法。
Serial Old 是 Serial 收集器的老年代版本,单线程收集器,采用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。

2、ParNew 收集器
并行收集器充分利用了多处理器的优势,采用多个 GC 线程并行收集。可想而知,多条 GC 线程执行显然比只使用一条 GC 线程执行的效率更高。 一般来说,与串行收集器相比,在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。ParNew 是针对新生代的垃圾回收器,采用“复制”算法,可以看成是 Serial 的多线程版本

3、Parallel
Parallel又分为Parallel Scavenge 收集器和Parallel Old收集器。
Parallel Scavenge 是针对新生代的垃圾回收器,采用“复制”算法,和 ParNew 类似,但更注重吞吐率。在 ParNew 的基础上演化而来的。 Parallel Scanvenge 收集器被誉为“吞吐量优先”收集器。 Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多线程收集器,采用“标记-整理”算法。

科普:吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如虚拟机总运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%。
4、CMS收集器
CMS(Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集,采用“标记-清除”算法,它的运作过程分为 4 个步骤:
1)初始标记(CMS initial mark) 2)并发标记(CMS concurrent mark) 3)重新标记(CMS remark) 4)并发清除(CMS concurrent sweep)

CMS收集器的问题是会造成很多的内存碎片。
5、G1 收集器
G1的出现,解决了CMS带来的内存碎片问题。
G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。在堆的结构设计时,G1 打破了以往将收集范围固定在新生代或老年代的模式,G1 将堆分成许多相同大小的区域单元,每个单元称为 Region,Region 是一块地址连续的内存空间,G1 模块的组成如下图所示:

堆内存会被切分成为很多个固定大小的 Region,每个是连续范围的虚拟内存。堆内存中一个 Region 的大小可以通过-XX:G1HeapRegionSize参数指定,其区间最小为 1M、最大为 32M,默认把堆内存按照 2048 份均分。
每个 Region 被标记了 E、S、O 和 H,这些区域在逻辑上被映射为 Eden,Survivor 和老年代。存活的对象从一个区域转移(即复制或移动)到另一个区域,区域被设计为并行收集垃圾,可能会暂停所有应用线程。
如上图所示,区域可以分配到 Eden,Survivor 和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous 区域是为了那些存储超过 50% 标准 Region 大小的对象而设计的,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。
G1收集过程分为四个步骤:初始标记、并发标记、最终标记、筛选回收。

- 初始标记(Initial Marking):标记GC Roots能直接关联到的对象。
- 并发标记(Concurrent Marking):从GC Roots开始对堆中的对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
- 筛选回收(Live Data Counting and Evacuation):对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行。
四、四种引用类型
本文多次提到了“引用”这个概念,在 Java 中有四种引用类型,这里也总结学习一下:
- 强引用(Strong Reference):如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。
- 弱引用(Weak Reference):它也是用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。
- 虚引用(Phantom Reference):也称为幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后提供了PhantomReference类来实现虚引用。