枚举根结点
当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的
安全点
-
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都产生对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得更高。
-
实际上,HotSpot并没有为每条指令都生成OopMap,而只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非所有地方都能停顿下来开始GC,只是在达到安全点时才能暂停
-
Safepoint的选择很重要,如果太少可能导致GC等待时间过长(空间可能满了爆掉),如果太频繁可能导致运行时性能问题(太多用的空间就多)。所以,安全点的选定基本上是以“是否具有让程序长时间执行的特征”为标准进行特定的 。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转、异常跳转等
-
对于Safepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来:抢占式中断和主动式中断(现在几乎没有虚拟机采用抢占式中断来暂停线程从而响应GC事件)
-
抢占式中断:它不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让它 “跑” 到安全点上
-
主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮循标志的地方和安全点是重合的【这个很关键,这样通过标志来中断刚好是在安全点上发生的】
安全区域
- 在使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际上情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但如果程序在“不执行”的时候呢?所谓程序不执行就是没有分配CPU时间,典型的例子就是处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,JVM也显示不太可能等待线程重新分配CPU时间。对于这种情况,就需要安全区域(SafeRegion)来解决了。
- 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。也可以把安全区域看作被扩展拉伸了的安全点
实际执行时:
- 1、在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了(不对它继续执行);
- 2、在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。
CMS收集器
-
CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
-
CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)。
注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。 -
1、初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
-
2、并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【啥意思?其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
-
3、重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS收集器的动作步骤
如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的:
image.png
解释:
- 首先左边那一排是指CPU的核心数量是4,指一个CPU最多能同时进行4个线程
- 初始标记:会触发Stop the world,所以可以看到没有执行的用户线程
- 并发标记:可以看到并发标记和用户线程并发执行,其中并发标记占据CPU一个核心
- 重新标记:每个线程都在重新标记
- 并发清理:可以看到并发清理和用户线程并发执行,其中并发清理占据CPU一个核心
- 重置线程:将CMS的垃圾回收线程恢复到最开始的状态,和用户线程并发执行
CMS收集器收集完整步骤
Phase1 :Initial Mark【初始标记】
Phase2 : Concurrent Mark 【并发标记】
Phase3 : Concurrent Preclean【并发预先清除】
Phase4 : Concurrent Abortable Preclean【并发可能失败的预先清除】
Phase5 : Final Remark【最终重新标记】
Phase6 : Concurrent Sweep【并发清除】
Phase7 : Concurrent Reset【并发重置】
(前5个是标记阶段细的五个子阶段)
-
Phase1 :Initial Mark【初始标记】
image.png
这个是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GC root引用或被年轻代存活对象所引用的所有对象。有对象是直接被GC ROOTS所指用的,有些对象是被年轻代引用的,都会被标记出来。
-
Phase2 : Concurrent Mark 【并发标记】
image.png
在这个阶段Garbage Collector会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到GC ROOTS遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。如下图:
在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化,如标黑的那个对象。 -
Phase3 : Concurrent Preclean【并发预先清除】
image.png
这也是一个并发阶段,与应用的线程并发运行,并不会stop应用的线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为Dirty,这也就是Card Marking。
在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了。
下面看下示意图:
上图中标红的则为Dirty,而能够被它所直接到达的对象也会被标记,标记完了则dirty card标记被清除,如下:
image.png -
Phase4 : Concurrent Abortable Preclean【并发可能失败的预先清除】
这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW(stop-the-world)中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等) -
Phase5 : Final Remark【最终重新标记】
这是第二个STW阶段,也是CMS中的最后一个,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,GC线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。
通常CMS的Final Remark阶段会在年代代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些。 -
Phase6 : Concurrent Sweep【并发清除】
image.png
这里不需要STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用,如图:
-
Phase7 : Concurrent Reset【并发重置】
这个阶段也是并发执行的,它会重设CMS内部的数据结构,为下次的GC做准备。
CMS垃圾收集器的优缺点
优点:并发收集、低停顿【注意:这里的停顿指的是停止用户线程】,Oracle公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。
缺点:
1、CMS收集器对CPU资源非常敏感。会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低
2、CMS收集器无法处理浮动垃圾,浮动垃圾简述是该对象本来不是垃圾,但在判断完后且清除之前,该对象却变成了垃圾对象,然而该对象却不能被回收掉,只能等下一次的GC
(就是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确的, 可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被回收但是没有被回收,只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。如果在应用中老年代增长不是太快,可能适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
3、收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
空间分配担保:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在Minor GC后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象空间的平均值作为经验值),则进行一次Full GC。
问题:有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compart呢?
由于在CMS垃圾回收过程中,并发清除线程和用户线程是并发执行的,如果把并发清除变成并发整理,会导致用户线程使用的对象地址发生改变,会使得用户线程不能正常执行。
用实践验证
当老年代使用CMS垃圾收集器时,新生代默认使用ParNew垃圾收集器
vm参数
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
package com.gc;
public class MyTest1 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] myAlloc1 = new byte[4 * size];
System.out.println("1111111");
byte[] myAlloc2 = new byte[4 * size];
System.out.println("22222222");
byte[] myAlloc3 = new byte[2 * size];
System.out.println("3333333");
byte[] myAlloc4 = new byte[4 * size];
System.out.println("4444444");
}
}
1111111
[GC (Allocation Failure) [ParNew: 6445K->835K(9216K), 0.0035794 secs] 6445K->4933K(19456K), 0.0046871 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
22222222
3333333
[GC (Allocation Failure) [ParNew (promotion failed): 7136K->6601K(9216K), 0.0035939 secs][CMS: 8646K->8634K(10240K), 0.0070299 secs] 11234K->11029K(19456K), [Metaspace: 3458K->3458K(1056768K)], 0.0110103 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8634K(10240K)] 15125K(19456K), 0.0005599 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
4444444
Heap
par new generation total 9216K, used 6977K[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 85% used [0x00000000fec00000, 0x00000000ff2d0598, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 8634K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3464K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 378K, capacity 388K, committed 512K, reserved 1048576K
网友评论