JVM 垃圾收集器之CMS

·  阅读 188

CMS是老年代的垃圾收集器

Concurrent Mark Sweep

CMS 理论部分

判断垃圾的方式

  • 判断垃圾一般是使用GC Roots,而不是使用引用计数器,因为引用计数器有循环引用问题。

枚举根节点

  • 当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在 HotSpot的实现中,是使用一组称为OopMap 的数据结构来达到这个目的的。

安全点

  • 在OopMap 的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得更高。
  • 实际上,HotSpot并没有为每条指令都生成OopMap,而只是在“特定的位置”记录了这些信息,就是OopMap改变的信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC只有在达到安全点时才能暂停。

image.png

  • Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负载。所以,安全点的选定基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 Safepoint。
  • 对于Safepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来: 抢占式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)

抢占式中断

  • 它不需要线程的执行代码主动去配合,在GC发生时首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。

主动式中断

  • 当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方,一般都是创建对象的时候会触发GC,设置为安全点也无可厚非。总之,主动式中断就是线程运行到某个标志时,自动停止。

现在几乎没有虚拟机采用抢占式中断来暂停线程从而响应GC事件。

安全区域

  • 在使用Safepoint 似乎已经完美地解决了如何进入GC的问题,但实际上情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但如果程序在“不执行”的时候呢﹖所谓程序不执行就是没有分配CPU时间,典型的例子就是处于Sleep状态或者blocked状态,这时候线程无法响应JVM的中断请求,JVM也显然不太可能等待线程重新分配CPU时间。对于这种情况,就需要安全区域(SafeRegion)来解决了。
  • 在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。 总之,在GC时,不用管在安全区的线程,但线程从安全区离开时,要确定GC是否已经完成,完成了线程才能离开安全区。

CMS垃圾收集器的内容

CMS 的主要内容

  • CMS (Concurrent Mark Sweep)收集器,以获取最短回收停顿时间为目标多数应用于互联网站或者B/S系统的服务器端上。
  • CMS是基于“标记—清除”算法实现的,整个过程分为4个步骤:
    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)
  • 其中,初始标记重新标记这两个步骤仍然需要"StopThe World";
  • 初始标记 只是标记一下GC Roots能直接关联到的对象,速度很快;
  • 并发标记阶段就是进行GC Roots Tracing 的过程。
    • 就是标记哪些对象不是垃圾,遍历引用链。
    • 因为是并发执行的,也就是说标记线程和用户线程一起执行,这就会出现问题,曾经被标记过的内容可能会改变,比如,标记线程标记某个对象不是垃圾,结果用户线程又再也不用那个不是垃圾的对象了。曾经的不垃圾变成了垃圾,所以就有了下面的重新标记阶段。
  • 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 优点
    • 并发收集、低停顿,Oracle 公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low PauseCollector)
  • 缺点
    • CMS 收集器对CPU资源非常敏感。
    • CMS 收集器无法处理浮动垃圾( Floating Garbage),可能出现“ConcurrnetMode Failure”失败而导致另一次Full GC的产生。如果在应用中老年代增长不是太快,可以适当调高参数- XX:CMSInitiatingOccupancyFraction的值来提高触发百分比以便降低内存回收次数从而获取更好的性能。 要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent ModeFailure”失败,性能反而降低。
    • 收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。
    • CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时,开启内存碎片的合并整理过程内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
    • 对于堆比较大的应用,GC 时间难以预估。

空间分配担保

  • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在 Minor GC后仍然存活,就需要老年代进行空间分配担保,把 Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为经验值),则进行一次Full GC。总之,就是要老年代空间连续,且要放得下对象,否则进行Full GC。

CMS 的详细步骤

一共细分为7个阶段

  • Phase 1: Initial Mark(初始标记)
  • Phase 2: Concurrent Mark(并发标记)
  • Phase 3: Concurrent Preclean(并发预清理)
  • Phase 4: Concurrent Abortable Preclean(并发中止预清理)
  • Phase 5: Final Remark(最终重标记)
  • Phase 6: Concurrent Sweep(并发清除)
  • Phase 7: Concurrent Reset(并发复位)

不是并发的步骤都会STW,就是Concurrent 开头的都不会STW

Phase 1: Initial Mark

  • 这个是CMS两次stop-the-world事件的其中一次.
  • 这个阶段的目标是:标记那些直接被GC Roots引用或者被年轻代存活对象所引用的所有对象。

别忘了CMS是老年代垃圾收集器。

image.png

Phase 2: Concurrent Mark

  • 在这个阶段Garbage Collector会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的GC Roots遍历查找。并发标记阶段,它会与用户的应用程序并发运行并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用

image.png

和阶段一的图比较,被黑圈标记的对象引用就改变了。

Phase 3: Concurrent Preclean

  • 这也是一个并发阶段,与应用的线程并发运行,并不会stop应用的线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为Dirty,这也称为Card Marking。

image.png

  • 在Preclean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了。

image.png

Phase 4: Concurrent Abortable Preclean

  • 这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW ( stop-the-world)中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直到满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)。

Phase 5: Final Remark

  • 这是第二个STW阶段也是CMS中的最后一个,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。
  • 通常CMS的 Final Remark阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些。

以上5个阶段是标记这阶段,下面就是清除阶段了。

Phase 6: Concurrent Sweep

  • 这里不需要STW,它是与用户的应用程序并发运。
  • 这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用。

image.png

Phase 7: Concurrent Reset

  • 这个阶段也是并发执行的,它会重设CMS内部的数据结构,为下次的GC做准备。

小结

  • CMS通过将大量工作分散到并发处理阶段来减少STW时间,在这块做得非常优秀,但是CMS也有一些其他的问题,就是有可能产生的内存碎片过多。
  • 该收集器的整体思路就是并发的标记大部分垃圾,用STW来精确的修正垃圾标记。

代码演示一下CMS

代码和VM参数

  • 以下为本次VM参数
-Xms20M  // 堆的最小值
-Xmx20M  // 堆的最小值
-Xmn10M  // 年轻代大小 
-XX:+PrintGCDetails // 打印GC 日志
-XX:SurvivorRatio=8 // Eden 占年轻代8
-XX:+UseConcMarkSweepGC // 老年代使用CMS垃圾收集器
复制代码
  • 代码
public class CMSTest {
    public static void main(String[] args) throws InterruptedException {
        int size = 1024*1024;// 1M
        byte[] bytes = new byte[4*size];
        System.out.println("第一次创建 4M");
        byte[] bytes1 = new byte[4*size];
        System.out.println("第二次创建 4M");
        byte[] bytes2 = new byte[4*size];
        System.out.println("第三次创建 4M");
        //Thread.sleep(2500);
        byte[] byte3 = new byte[3*size];
        System.out.println("第四次创建 3M");
    }
}
复制代码

image.png

也可以观察到CMS是并行的,因为CMS的日志输出是随意的,CMS的大部分步骤并不会影响程序的运行。所以print语句和CMS 日志输出的顺序不一。

分类:
后端
标签:
分类:
后端
标签: