1. 概述

在上篇中,我们提到了什么是 GC、GC 的原理、何时该 GC 等问题,但是具体的垃圾回收算法没有提到。在本篇博客中,我想主要讲述以下几方面内容:

  1. 典型的垃圾回收算法
  2. 典型的垃圾收集器
  3. 内存分配与回收策略

注:本文有部分内容直接摘抄自《深入理解 Java 虚拟机》,也有部分内容直接引用自http://www.cnblogs.com/dolphin0520/p/3783345.html

2. 典型的 GC 算法

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始 GC ,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于 Java 虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

2.1 Mark-Sweep(标记-清除)算法

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

标记-清除算法示意图
标记-清除算法示意图

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.2 Copying(复制)算法

为了解决 Mark-Sweep 算法的缺陷,Copying 算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

复制算法示意图
复制算法示意图

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

2.3 Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

标记-整理
标记-整理

2.4 Generational Collection(分代收集)算法

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1 的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉Eden和刚才使用过的 Survivor 空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的 Mark-Compact算法。

3. 典型的垃圾收集器

垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下 HotSpot(JDK 7) 虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

3.1 Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial 收集器是针对新生代的收集器,采用的是 Copying 算法,Serial Old 收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

3.2 ParNew

ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.3 Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

3.4 Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

3.5 CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

3.6 G1

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

4. 内存分配及回收策略

4.1 对象优先在 Eden 分配

大多数情况下,对象在新生区 Eden 区中分配,当 Eden 没有足够空间分配时,虚拟机将发起一次 Minor GC,GC后将已有对象放入Survivor中,若 Survivor 空间不足,则通过分配担保机制提前转移到老年代。

4.2 大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的Java对象,例如较长的字符串和较长的数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个值的参数直接在老年代分配。

4.3 长期存活的对象进入老年代

虚拟机既然采用了分代收集的思想来管理内存,那么内存回收时就必须能识别对象放在新生代,哪些对象放在老年代。为了做到这点,虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次 Minor GC 然后仍然存活,并且能被 Survivor 容纳的话,将被移到 Survivor 中,并且对象年龄设为 1 。对象在 Survivor 中每过一次 Minor GC,年龄就增加一岁。当年龄增加到一定程度,就会晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

4.4 动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到 MaxTenuringThreshold 的要求。

4.5 空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。若不成立,则虚拟机会查看HandlePromotionFailure设置值是否担保失败,若允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小(相当于一个经验值,不保证可以成功),如果大于,尽管这次 Minor GC 冒险,也会尝试进行一次 Minor GC ;如果小于,或者 HandlePromotionFailure 设置为不允许冒险,那么需要进行一次 Full GC。

5. 小结

  • 典型的 GC 算法包括标记-清除(mark and sweep)、复制(copying)、标记-整理(mark and Compact)。目前的 JVM 大都使用分代收集算法。对于新生代都采取 Copying 算法,老年代采取标记-整理算法。

  • 一般新生代被划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉Eden和刚才使用过的 Survivor 空间。

  • 对象优先在 Eden 分配,大对象和长期存活的对象会直接进入老年代。在发生 Minor GC 之前,会检查老年代的最大可用空间来进行空间分配担保。