java虚拟机垃圾收集

Java虚拟机除了上一篇文章当中提到的内存管理外,另外一个重要的知识便是垃圾收集。在进行收集垃圾之前,我们首先要考虑一下三个问题:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

哪些内存是垃圾

一块内存是垃圾,也就是说在该内存上的对象已经不再使用了,需要去进行回收,那么,如何来判别对象是否已经不再用了,需要进行回收呢?这里有两个方法:

  • 引用计数算法
    给对象添加一个引用计数器,当每有一个地方引用它时,引用器值加1,当引用失效时,计数器减1。当计数器为0的时候,也就表示对象不可能再被使用了。这个方法实现简单,判定效率也很高。但是存在一个问题,就是它很难解决对喜爱那个之间互相循环引用的问题。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ReferenceGc {
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    }

    ...

    public static void testGC() {
    ReferenceGc objA = new ReferenceGc();
    ReferenceGc objB = new ReferenceGc();
    objA.instance = objB;
    objB.instance = objA;
    }

    在函数testGC创建了两个对象,在函数退出之后,objA和objB已经不会再被使用了,但是由于它们之间存在着引用,故引用计数算法得出的结论是objA与objB还在被引用,也就不会将这两个对象作为垃圾。

  • 可达性分析算法
    算法的基本思想如下:选择一些GC Roots作为起点,根据引用关系遍历所有能够到达的对象,遍历的路经称作引用链,当一个对象与GC Roots没有引用链的时,则证明了该对象是不可用的,也就是需要回收的垃圾。
    GC Roots的选择如下:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对对象
    • 方法区中产量引用的对象
    • 本地方法栈中JNI引用的对象

如对象到GC Roots之间存在引用链,则根据引用的类型不同,是否进行回收也存在着区别,四个引用强度一次逐渐减弱:
强引用
即Object obj = new Object();即为强引用。

软引用
描述一些没有用但是非必需的对象,对于软引用,除非系统将要发生内存溢出异常,才会去进行回收。

弱引用
被弱引用引用的对象会被进行回收

虚引用
一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用老获得一个对象实例。为对象设置虚引用的唯一目的就是能在对象被收集器回收时收到一个系统通知。

垃圾收集算法

几种主要的垃圾收集算法:

  • 标记-清除算法
    对垃圾进行标记,然后清除垃圾,但是由于垃圾不一定是连续的,会存在碎片
  • 复制算法
    将内存分为成两个部分,在一起一块上分配内存创建对象,进行垃圾收集时,将存活的对象复制到另外一块内存上。
  • 标记-整理算法
    对垃圾进行标记,然后清除垃圾,然后整理内存,去除碎片
  • 分代收集算法
    将内存分成新生代和老生代,对不同的代使用不同的收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中没有对垃圾和搜集其应该如何实现做规定,因此不同的厂商和不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。这里讨论的收集器是基于JDK1.7 Update14之后的HotSpot虚拟机器。
GC 分类
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

几个概念

并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

新生代收集器

Serial收集器

Serial收集器是最基本、发展历史最悠久的新生代收集器,也是原理最简单的收集器。它是一个采用复制算法的单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止。
SerialNew收集器
该收集器目前是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

ParNew 收集器

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同。
ParNew收集器
该虚拟机是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

老年代收集器

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。收集器的主要意义也是在于给Client模式下的虚拟机使用。Serial Old收集器的工作过程如图所示:
SerialOld收集器

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。Parallel Old收集器的工作过程如图所示:
Parallel Old收集器

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

CMS收集器的工作过程如图所示:
CMS收集器

优点

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  • 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。

G1收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  • 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

G1收集器的内存模型

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。

工作过程

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率,流程图如下:
    G1收集器

总结

根据上述几种垃圾收集器的工作原理及工作区域,进行一个归总:

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS

参考资料 : 深入理解Java虚拟机(第2版)

欢迎关注个人公众号:
个人公号