先回顾一下上一篇介绍的JVM中常见几种垃圾收集算法:
- 标记-清除算法(Mark-Sweep)。
- 复制算法(Copying)。
- 标记整理算法(Mark-Compact)。
- 分代收集算法(Generational Collecting)。
如果说收集算法是内存回收的方法论。那么垃圾收集器就是内存回收的具体实现。不同的厂商、不同的版本的虚拟机提供的垃圾收集器会有很大差别,目前讨论的收集器基于JDK1.7 Update 14之后的HotSpot虚拟机。这个虚拟机包含的所有垃圾收集器以及其作用范围如图:
7种作用于不同分代的收集器,可以连线的两个说明可以搭配使用。不同的收集器作用于不同的分代,这是说明没有一个收集器能在任何场景下都完美适用,肯定都是有舍有得,通过了解和分析垃圾回收器是就是为了让我们选择出对具体应用最合适的收集器。
Serial收集器
Serial收集器是发展历史最悠久的收集器,在jdk1.3之前是新生代唯一的选择,它是一个单线程收集器,单线程并不代表在进行垃圾回收时会只用一个线程去进行垃圾回收,而重要的是在它进行垃圾收集时,必须暂停其他所有线程,直到收集结束。这就是传说中的“Stop The World”。
运行示意图如下:
虽然这个过程是一个用户不可见的执行过程,但是确实也给用户带来了不良体验,从Serial到G1目前都还没有办法彻底消除停顿时间。Serial收集器虽然有这个大弊端,但是对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。对于运行在Client模式下的虚拟机来说Serial收集器是一个很好的选择。
ParNew收集器
ParNew收集器其实是Serial收集器的多线程版本,除了使用了多线程进行垃圾收集之外,其他的内容大致和Serial一样,例如:配置参数、收集算法、Stop The World对象分配规则、回收策略等。在实现上大部分代码也重合。而且ParNew收集器也是作用在新生代。
运行示意图如下:
ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新,但它趋势许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,使用的复制算法,并且也是多线程并行收集器,功能上和ParNew收集器类似,但是两者个关注点是不同的,ParNew等收集器的关注点是尽可能的缩短用户线程的停止时间,而Parallel Scavenge收集器的关注点是让JVM达到一个可控制的吞吐量。所谓吞吐量就是:CPU运行用户代码的时间与CPU消耗时间的比值。即吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),运行用户代码时间99分钟,垃圾收集时间1分钟,那吞吐量就是99%。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,即垃圾收集时间占总时间的比率,默认99,就是允许最大1%(1/(1+99))的垃圾收集时间。
运行示意图如下:
Serial Old收集器
Serial Old收集器是运行在老年代的收集器,相当于Serial的老年代版本,也是单线程收集器。使用的“标记-整理”算法,主要也是Client模式下的虚拟机使用(单个CPU的环境),在Server模式下主要还有两大用途:一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案。
Serial Old 收集器运行示意图如下:
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在JDK1.6中才开始提供,在此之前,新生代选择了Parallel Scavenge收集器,老年代只能选择Serial Old,因为Parallel Scavenge是多线程收集器,但是到了老年代只能用Serial Old收集器,这样相当于只能在新生代达到提高吞吐量的效果。直到老年代也提供了这种以提高吞吐量为主的收集器,新生代使用Parallel Scavenge收集器,老年代使用Parallel Old收集器就形成了以吞吐量优先的收集器组合。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑这种场合。
Parallel Old收集器运行示意图如下:
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以缩短回收停顿时间为目标,并且适用于老年代的收集器。
它运作过程相对复杂一些,主要分为4个步骤:
- 初始标记(CMS inital mark)
- 并发标记 (CMS concurrent mark)
- 重新标记 (CMS remark)
- 并发清除 (CMS concurrent sweep)
初始标记和重新标记是需要线程停顿的。初始标记仅仅只是标记一些GC Roots能直接关联到对象,并发标记是进行GC Roots Tracing(追踪)的过程,而重新标记是为了修正并发标记时因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
运行过程示意图如下:
CMS收集器的优点从运行过程中就可以看出来,并发收集,低停顿。并发标记和并发清除可以和用户线程一起运行这样也是降低了用户应用程序的停顿时间。
但是CMS收集器也还是远远没有达到完美的程度, 它有以下3个明显的缺点:
CMS收集器对CPU资源非常敏感。在并发标记和并发清除时是和用户线程一起运行的,收集过程中肯定占用了用户程序的CPU资源。默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程,影响用户,降低吞吐量。
CMS收集器无法处理浮动垃圾。在并发清除阶段,用户程序并没有停止,所以还会继续产生垃圾,而这部分垃圾只能等待着下一次收集时才能进行回收。
CMS收集器会产生空间碎片。因为CMS收集器是基于“标记-清除”算法实现的,所以在进行大量的垃圾回收时,会产生很多不连续的内存空间。这是使用“标记-清除”算法都会有的缺点。
G1收集器
G1(Garbage First)收集器是目前最新的收集器了,java9以及java10默认的垃圾收集器就是G1。在JDK6u14中就有Early Access版本的G1收集器提供开发人员试用。到JDK 7u4才算是正是发布。G1是基于“标记-整理”算法来实现的,可以独立的维护新生代以及老年代两个部分。
与其他的GC收集器相比,G1有如下特点:
并行与并发,G1充分利用多CPU资源来缩短停顿时间,即执行GC过程中用户程序扔可继续执行。
分代收集,虽然G1可以不需要与其他收集器配合就可以独立管理整个GC堆,但分代感念在G1中依然保留,这样可以让G1采用不同的方式来处理新生代和老年代的对象。
空间整合,因为G1是基于“标记-整理”算法实现的,所以不会产生空间碎片,内存空间很好的整合起来了。
可预测的停顿,这个特点是相对CMS收集器的一个优势,G1可以让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在牢记收集上的时间不得超过N秒。
前面介绍的几个收集器,要么是作用在新生代,要么是作用在老年代,G1作用于整个堆,它将整个堆分为多个大小相等的独立区域(Region),虽然仍保留了分代,但不再是物理隔离,都是一部分Region的集合。
G1收集器运行过程大致分为以下几步:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
初始标记阶段和CMS收集器类似,也是仅仅标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时可以在正确的Reginon中创建新对象。并发标记阶段是对堆中对象进行可达性分析,找出存活对象,这个阶段可以与用户线程并发进行,而最终标记阶段是为了修正在并发标记期间因为用户程序并发运行,而导致标记产生变动的那部分标记记录,这阶段需要用户线程停顿,但是可并发进行。在最后的筛选回收阶段首先对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间来制定回收计划。
运行过程示意图如下:
JDK目前有的这些收集器差不多都介绍完了,下面来看看各个版本的JDK默认的都是使用的什么垃圾收集器吧。
这段代码是打印出当前jdk中使用的是什么垃圾回收器
@org.junit.Test public void myTestGC(){ Listlist = ManagementFactory.getGarbageCollectorMXBeans(); for(GarbageCollectorMXBean gb:list){ System.out.println("垃圾回收器:"+gb.getName()); } }
我的的IDEA目前配置的jdk版本是jdk1.8.0_144,运行结果如下:
PS Scavenge代表的是Parallel Scavenge收集器,PS MarkSweep 代表的是Parallel Old收集器(一开不知道是表示的Parallel Old还跑到知乎上面去提问,结果还是自己找到的答案)这说明JDK8默认使用的是以吞吐量优先的这两个收集器组合。
下面我把IDEA的JDK版本换成JDK9,运行结果如下:
这个就可以直接了当的看出来 了,JDK9默认的垃圾收集器是G1。
下面我把IDEA的JDK版本换成JDK10,运行结果如下:
这说明JDK10默认的垃圾收集器也是G1。
下面我把IDEA的JDK版本换成JDK1.7,运行结果如下:
这表示JDK1.7默认的垃圾收集器也是Parallel Scavenge和Parallel Old。
因为下载不到JDK1.6以及再之前的JDK了,所以就先看这几个版本的吧。如果找到了,后续再补上。