在开始介绍CMS和G1前,咱们能够剧透几点:html
CMS和G1做为垃圾收集器里的大杀器,是须要好好弄明白的,并且面试中也常常被问到。java
但愿你们带着下面的问题进行阅读,有目标的阅读,收获更多:面试
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是由于CMS收集器工做时,GC工做线程与用户线程能够并发
执行,以此来达到下降收集停顿时间的目的。算法
CMS收集器仅做用于老年代的收集,是基于标记-清除算法
的,它的运做过程分为4个步骤:安全
其中,初始标记
、从新标记
这两个步骤仍然须要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而从新标记阶段则是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始阶段稍长一些,但远比并发标记的时间短。bash
CMS以流水线方式拆分了收集周期,将耗时长的操做单元保持与应用线程并发执行。只将那些必需STW才能执行的操做单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就能够完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和从新标记),达到了近似并发的目的。并发
CMS收集器优势:并发收集、低停顿。oracle
CMS收集器缺点:app
CMS收集器之因此可以作到并发,根本缘由在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来讲是难以接受的,所以新生代的收集器并未提供CMS版本。jsp
另外要补充一点,JVM在暂停的时候,须要选准一个时机。因为JVM系统运行期间的复杂性,不可能作到随时暂停,所以引入了安全点的概念。
安全点,即程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。
安全点的初始目的并非让其余线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便可以“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便可以在垃圾回收的同时,继续运行这段本地代码。
程序运行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生Safepoint。
对于安全点,另外一个须要考虑的问题就是如何在GC发生时让全部线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。
两种解决方案:
抢先式中断(Preemptive Suspension)
抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
主动式中断(Voluntary Suspension)
主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起。轮询标志的地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。
指在一段代码片断中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也能够把Safe Region看做是被扩展了的Safepoint。
G1从新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么作的目的是在进行收集时没必要在全堆范围内进行,这是它最显著的特色。区域划分的好处就是带来了停顿时间可预测的收集模型:用户能够指定收集操做在多长时间内完成。即G1提供了接近实时的收集特性。
G1与CMS的特征对好比下:
特征 | G1 | CMS |
---|---|---|
并发和分代 | 是 | 是 |
最大化释放堆内存 | 是 | 否 |
低延时 | 是 | 是 |
吞吐量 | 高 | 低 |
压实 | 是 | 否 |
可预测性 | 强 | 弱 |
新生代和老年代的物理隔离 | 否 | 是 |
G1具有以下特色:
在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1将堆分红许多相同大小的区域单元,每一个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成以下图所示:
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽可能划分2048个左右、同等大小的Region,这一点能够参看以下源码。其实这个数字既能够手动调整,G1也会根据堆大小自动进行调整。
#ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP
#define SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP
#include "memory/allocation.hpp"
class HeapRegionBounds : public AllStatic {
private:
// Minimum region size; we won't go lower than that. // We might want to decrease this in the future, to deal with small // heaps a bit more efficiently. static const size_t MIN_REGION_SIZE = 1024 * 1024; // Maximum region size; we don't go higher than that. There's a good // reason for having an upper bound. We don't want regions to get too
// large, otherwise cleanup's effectiveness would decrease as there // will be fewer opportunities to find totally empty regions after // marking. static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024; // The automatic region size calculation will try to have around this // many regions in the heap (based on the min heap size). static const size_t TARGET_REGION_NUMBER = 2048; public: static inline size_t min_size(); static inline size_t max_size(); static inline size_t target_number(); }; #endif // SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP 复制代码
G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会经过一个合理的计算模型,计算出每一个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的状况下,老是能选择一组恰当的Regions做为收集目标,让其收集开销知足这个限制条件,以此达到实时收集的目的。
对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,若是发现符合以下特征,能够考虑更换成G1收集器以追求更佳性能:
原文以下: Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.
- More than 50% of the Java heap is occupied with live data.
- The rate of object allocation rate or promotion varies significantly.
- Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)
G1收集的运做过程大体以下:
停顿线程
,但耗时很短。停顿线程
,可是可并行执行。全局变量和栈中引用的对象是能够列入根集合的,这样在寻找垃圾时,就能够从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是记忆集
(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的不少开源都是源自Remembered Set,例如,它一般约占Heap大小的20%或更高。而且,咱们进行对象复制的时候,由于须要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。
有个场景,老年代的对象可能引用新生代的对象,那标记存活对象的时候,须要扫描老年代中的全部对象。由于该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又作全堆扫描?成本过高了吧。
HotSpot给出的解决方案是一项叫作卡表
(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,而且维护一个卡表,用来存储每张卡的一个标识位。这个标识位表明对应的卡是否可能存有指向新生代对象的引用。若是可能存在,那么咱们就认为这张卡是脏的。
在进行Minor GC的时候,咱们即可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成全部脏卡的扫描以后,Java虚拟机便会将全部脏卡的标识位清零。
想要保证每一个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机须要截获每一个引用型实例变量的写操做,并做出对应的写标识位操做。
卡表能用于减小老年代的全堆空间扫描,这能很大的提高GC效率。
咱们能够看下官方文档对G1的展望(这段英文描述比较简单,我就不翻译了):
Future: G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.
查了下度娘有关G1的文章,绝大部分文章对G1的介绍都是停留在JDK7或更早期的实现不少结论已经存在较大误差了,甚至一些过去的GC选项已经再也不推荐使用。举个例子,JDK9中JVM和GC日志进行了重构,如PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会致使JVM没法启动。
本文对CMS和G1的介绍绝大部份内容也是基于JDK7,新版本中的内容有一点介绍,倒没作过多介绍(本人对新版本JVM尚未深刻研究),后面有机会能够再出专门的文章来重点介绍。
《深刻理解Java虚拟机》 《HotSpot实战》 《极客时间专栏》