垃圾收集器-CMS & G1
CMS
CMS收集器是以获取最短回收停顿为目标的垃圾收集器。是基于标记-清除算法实现的,它的运行过程相对于前面的几种垃圾收集器来说要更复杂点,整个过程分为4个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中,初始标记和重新标记这两个步骤仍然需要STW(Stop The World)。
- 初始标记仅仅标记
GC Roots
能关联到的直接对象 - 并发标记阶段就是从关联对象开始遍历整个对象图的过程,这个过程耗时很长,但不会影响用户线程的执行
- 重新标记则是为了修正并发标记节点期间,因用户线程而导致标记变动的一部分对象记录
- 并发清理就是回收掉那些已经死亡的对象,也是可以跟用户线程并发执行的
由于整个过程中,耗时最长的两个阶段:并发标记和并发清除,都可以与用户线程并发执行,所以总体来说,CMS垃圾收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器是HotSpot虚拟机追求低停顿的第一次尝试,尽管效果很显著,但是仍有比较明显的缺点:
- CMS对处理器的资源比较敏感,在并发阶段,它虽然不会导致用户线程停顿,但却会因为占有了一部分线程而导致应用程序变慢,降低总吞吐量。
- CMS无法处理浮动垃圾。在并发标记和并发清理阶段,用户线程是还在运行的,程序自然会产生新的垃圾对象,但这些垃圾是出现在标记过程之后的,CMS无法在当次收集中清理掉他们,只能留到下次垃圾收集时再清理。同时CMS在垃圾收集阶段还要保证用户线程正常运行,因此不能像其他收集器那样等到老年代完全填满时再进行收集,必须预留一部分空间给用户线程使用。
- CMS是基于标记清除算法的,这类算法我们前面提到过,容易产生大量内存碎片
G1
作为CMS收集器的替代者和继承人,设计者们希望能建立一个可以支撑“停顿预测模型”的收集器。这里“停顿预测模型”是指:支持在长度为M的时间片段内,消耗在垃圾收集的时间不超过N。
那要怎么做才能实现这个目标呢?首先要在思想上有一个改变,在以前,所有的垃圾收集器的目标范围要么是整个新生代,要么就是整个老年代,再要么就是整个Java堆。 而G1跳出了这个限制,它可以面向堆内存的任何区域来组成回收集合(Collection Set)。衡量标准也不再是它处于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
G1不再坚持固定大小和固定数量的分代区域划分,而是把连续的Java堆划分成多个大小相等的独立区域Region,每个Region可以分别扮演Eden,Survivor和Old。
Region中还有一类特殊的Humongous区域,专门存储大对象。G1只要认为一个对象的大小超过了Region的一半即可判定为大对象。
每个Region的大小可以通过参数-XX:G1HeapRegionSize
设置,取值范围在1MB-32MB。G1的大多数行为都把Humongous Region当成老年代看待。
虽然G1仍然保留新生代和老年代的概念,但是不再是固定区域,它们都是一系列区域(不一定连续)的动态集合。
G1会跟踪各个Region里面的内存垃圾的多少,对应回收的空间大小和回收时间花费,然后在后台维护一个优先级列表,每次回收时,会尽可能回收消耗时间长的区间,同时,每次回收时间不会超过参数-XX:MaxGCPauseMills
指定的时间(默认200ms)。
G1的实现并非一帆风顺,实际在实现过程中有以下难点需要处理:
- 将Java堆分成多个Region后,跨Region引用的对象如何解决?解决思路我们已经有了,就是通过记忆集来解决,但实际上G1的实现要复杂的多,它为每个Region都实现了记忆集,这些记忆集会记录别的Region指向自己的指针,并标记这些指针分别在哪些卡页下。 G1的记忆集本质上还是一个Map结构,key是别的Region的起始地址,value是一个集合,存储了卡表的索引号。这种双向的卡表结构(卡表是“我指向谁”,这种结构还记录了谁指向我),比原来的实现更加复杂,因此G1要比其他收集器要消耗更多额外的内存空间。
- 在并发标记阶段如何保证收集线程与用户线程互相不干扰?这里G1直接采用的原始快照方法去实现的。此外,垃圾收集对用户线程的影响还体现在回收过程中新对象创建的内存分配上。 G1为每个Region设计了两个个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用户新对象分配,新对象的创建都必须要在这两个指针位置以上,G1默认这个地址范围内的对象是存活的。
一般来说,G1的运行过程大致可以划分成四个阶段:
- 初始化标记,只是标记下
GC Roots
能访问到的直接对象,并修改TAMS指针,这个阶段需要STW,但时间很短 - 并发标记,从
GC Roots
开始对堆中的对象进行可达性分析,递归扫描整个堆的对象图,找出要回收的对象,这个阶段时间周期很长,但可以与用户线程并发执行。 - 最终标记,重新处理STAB(原始快照)中有变化的引用对象,需要STW
- 筛选回收,根据各个Region的回收数据,评估值得回收的Region,并执行清理操作,并把这些Region中空闲的对象移动到空闲的Region中,这个阶段是STW的
相比CMS,G1的优点有很多,可以指定最大停顿时间,按收益动态回收Region,并且与CMS不同,G1整体上看是使用标记整理算法,但从局部Region与Region之间是使用标记复制算法,这也意味这G1不会产生内存碎片。 但是缺点也很明显:如需要占用更多额外内存空间,额外的记忆集也带来了更高的运行负载。