vlambda博客
学习文章列表

垃圾回收的实践者-垃圾回收器


如果说垃圾回收算法是垃圾回收的方法论,那垃圾回收器就是垃圾回收的实践者。

没有万能的收集器,只有针对具体应用最合适的收集器。

就目前来说,CMS和G1这两款收集器相对复杂但使用广泛。



《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。



在JDK 7 Update 4之后(在这个版本中正式提供了商用的G1收集器)、JDK 11正式发布之前,OracleJDK中的HotSpot虚拟机所包含的全部可用的垃圾收集器如下图:


垃圾回收的实践者-垃圾回收器


  • 如果两个收集器之间存在连线,就说明它们可以搭配使用

  • 图中的两个JDK9表示这条连线在JDK9之后断开,不能再配合使用了

  • 图中收集器所处的区域,则表示它是属于新生代收集器或是老年代收集器





Serial串行收集器



Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。Serial收集器工作过程如下图所示。

垃圾回收的实践者-垃圾回收器



主要特性:
  • 单线程工作

  • 必须暂停其他所有工作线程,直到收集结束(被称作Stop The World



对于Stop The World带给用户的恶劣体验,hotspot虚拟机团队一直在优化与创新,因此诞生了诸多的垃圾收集器,Serial收集器仍有着优于其他收集器的地方:

  • 简单而高效

  • 对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的

  • 对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销而更加高效

  • Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择



ParNew并行收集器




ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示。


垃圾回收的实践者-垃圾回收器



ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程上下文切换开销,该收集器在通过超线程技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。
当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多(比如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常普遍)的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数




Parallel Scavenge收集器



Parallel Scavenge收集器与ParNew收集器的共同点:

  • 新生代收集器

  • 基于标记复制算法

  • 并行收集的多线程收集器


不同点:

  • Parallel Scavenge的特点是它的关注点与其他收集器不同

  • 其它收集器尽可能地缩短垃圾收集时用户线程的停顿时间

  • 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量


什么是吞吐量:
处理器用于运行用户代码的时间与处理器总消耗时间的比值,如下图所示。



垃圾回收的实践者-垃圾回收器


如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
  • 关注停顿时间适用于需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验

  • 关注高吞吐量适用于最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务


至于Parallel Scavenge如何控制吞吐量,我就不深入了,基本上用不到。



Serial Old收集器



Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

Serial Old收集器的工作过程如下图所示。



垃圾回收的实践者-垃圾回收器




Parallel Old收集器



Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记整理算法实现。在注重吞吐量或者处理器资源较为稀缺的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器这个组

合。

Parallel Old收集器的工作过程如下图所示。


垃圾回收的实践者-垃圾回收器




CMS收集器




CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

大部分Java应用都是基于B/S的,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

Mark Sweep表示CMS收集器是基于标记清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记(CMS initial mark):Stop The World

  2. 并发标记(CMS concurrent mark):no

  3. 重新标记(CMS remark):Stop The World

  4. 并发清除(CMS concurrent sweep):no


CMS工作过程如下图所示。


垃圾回收的实践者-垃圾回收器


  • 初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快

  • 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图,这个过程耗时较长但是可以与用户线程并发运行

  • 重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长,但也远比并发标记阶段的时间短

  • 并发清除阶段清理删除掉标记阶段判断的已死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的



CMS停顿时间短的原因(优点):

  • 由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程并发运行,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的





CMS缺点:

  • CMS在并发阶段会占用一部分线程,虽然不会导致用户线程停顿,但会导致应用程序变慢

  • CMS收集器无法处理浮动垃圾(并发清楚阶段,由于程序还在运行会有新的垃圾产生,即浮动垃圾),只能等下一次垃圾回收处理

  • 基于标记清除法,会产生大量内存碎片




G1收集器



Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率,而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。
G1收集器是全功能的垃圾收集器,采用标记整理法与标记复制法。
由于CMS的历史包袱,规划JDK 10功能目标时,HotSpot虚拟机提出了统一垃圾收集器接口,将内存回收的行为与实现进行分离,CMS以及其他收集器都重构成基于这套接口的一种实现,由此扔掉了CMS的历史包袱

G1有一个思想上的改变,G1之前的垃圾收集器回收的目标范围要么是整个新生代(Minor GC)、要么就是整个老年代(Major GC)、要么就是整个Java堆(Full GC),而G1可以面向任一区域,哪块内存中存放的垃圾数量最多,回收收益最大,就回收哪块儿。

虽然G1仍是遵循分代收集理论,但G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间,收集器能够对扮演不同角色的Region采用不同的策略去处理。虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合
Region中还有一类特殊的Humongous区域,专门用来存储大对象,G1认为只要大小超过了半个Region容量的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待,如下图所示。

垃圾回收的实践者-垃圾回收器



G1将Region作为单次回收的最小单元,每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。



G1收集器的运作过程大致可划分为以下四个步骤:

  1. 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,这个阶段需要停顿线程,但耗时很短。

  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。

  3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发标记结束后有引用变动的记录。

  4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成。





G1的工作过程如下图所示。


垃圾回收的实践者-垃圾回收器


如何选择合适的垃圾回收器?




其实“垃圾收集器”这个名字并不能形容它全部的职责,更贴切的名字应该是“自动内存管理子系统”。

一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责:

  • 堆的管理与布局
  • 对象的分配
  • 与解释器的协作
  • 与编译器的协作
  • 与监控子系统协作
等职责,其中至少 堆的管理和对象的分配 这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的内容。

如何选择一款适合自己应用的收集器呢?主要是看应用程序的主要关注点是什么:

  • 如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点

  • 如果是服务类或WEB应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点

  • 如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的


衡量垃圾收集器的三项最重要的指标是:
  • 内存占用

  • 吞吐量

  • 延迟

三者共同构成了一个“不可能三角”,即三个方面同时“完美”的收集器几乎是不可能的,一款优秀的收集器通常最多可以同时达成其中的两项。


在内存占用、吞吐量和延迟这三项指标里,延迟的重要性日益凸显,越发备受关注。其原因是随着计算机硬件的发展、性能的提升,我们越来越能容忍收集器多占用一点点内存;硬件性能增长,吞吐量会更高,但对延迟则不是这样。硬件规格提升,准确地说是内存的扩大,对延迟反而会带来负面的效果,比如虚拟机要回收完整的1TB的堆内存,毫无疑问要比回收1GB的堆内存耗费更多时间。由此,我们就不难理解为何延迟会成为垃圾收集器最被重视的性能指标了。




实用技巧



前面讲的都是理论,实在太枯燥了,不妨先看一些关于GC的实用技巧。

首先,我们可以用如下命令查看当前版本的jvm默认使用的是什么垃圾回收器


java -XX:+PrintCommandLineFlags -version


该命令输出结果如下图所示。


垃圾回收的实践者-垃圾回收器


重点看如下输出结果。


-XX:InitialHeapSize=264737408 -XX:MaxHeapSize=4235798528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC


可以看到-XX:+UseParallelGC,这是因为JDK9之前server模式下默认使用的是Parallel Scavenge+Serial Old组合(我的是JDK8)。

如果想要使用G1怎么办?可以通过显示指定-XX:+UseG1GC。而UseG1GC是JDK9之后server模式下的默认值


另外,还有两条打印GC日志的虚拟机参数如下:

-XX:+PrintGC

-XX:+PrintGCDetails


下面通过一个例子来运行一下看看。

首先,我的代码如下(就是一直循环创建对象)。


public class GCTest {
    private Integer id=999;
    private String name="zhangsan";

    public static void createObjectByFor(long time) throws InterruptedException {
        for (;;){
            new GCTest();
            Thread.sleep(time);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        createObjectByFor(1L);
    }
}


然后在idea中指定一下虚拟机参数,如下图所示。


垃圾回收的实践者-垃圾回收器


运行程序,控制台打印出来的GC日志如下图所示。


垃圾回收的实践者-垃圾回收器


通过GC日志可以看到,虚拟机使用G1收集器回收垃圾。

这一部分主要讲的是如何使用几个重要的虚拟机参数,其实虚拟机参数太多了,一般的中小型项目根本用不到,而且JDK9前后虚拟机参数的使用方式大不一样,我这里就不一一列举了,有兴趣或有需要的小伙伴可以直接到oracle官网上查阅。



这么多垃圾收集器,是不是看的有点脑壳疼,我也是。尤其是G1回收器,书上描述的内容特别多,经过我的一番吸收消化,删减不重要的部分,留下的都是精华,主要是掌握G1回收器的思想。

到这里关于垃圾回收器的分享就结束了,如果你能全部看完看懂,碾压面试官应该是没问题了,划重点:G1。




喜欢就点个在看再走吧