看看你是如何把握老年代收集器

Posted 小孟的coding之旅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了看看你是如何把握老年代收集器相关的知识,希望对你有一定的参考价值。

Serial Old收集器

Serial Old 收集器,也叫串型老年代收集器。Serial Old 的收集器,可以认为是 Serial 收集器的老年代版本,属于多线程收集器,但是它采用的是标记-整理算法。执行过程大致如下图所示:

可以发现,Serial Old 收集器与 Serial 收集器除使用的算法不同以外,其他和 Serial 收集器都是一样的。

Serial Old 收集器有哪些适用场景呢?Serial Old 收集器可以和 Serial 收集器、ParNew 收集器以及 Parallel Scavenge 收集器这三个新生代的垃圾收集器配合使用(值得一提的是:在 JDK1.5 及之前,与Parallel Scavenge 收集器搭配使用(JDK1.6 有 Parallel Old收集器可搭配 Parallel Scavenge 收集器))。另外如果你的老年代使用的是 CMS 收集器,并且出现故障的时候也会使用 Serial Old 收集器作为后备,作为后备的情况,我们在探讨到c ms收集器的时候,再来详细探讨。

Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老生代版本,属于多线程收集器,采用标记-整理算法。Parallel Old 收集器和 Parallel Scavenge 收集器同样考虑了吞吐量优先这一指标,非常适合那些注重吞吐量和 CPU 资源敏感的场合。执行过程大致如下图所示:

从图中可以发现 Parallel Old 收集器和 Parallel Scavenge 收集器基本上是一样的。

Parallel Old 收集器的特点是它只能和 Parallel Scavenge 配合使用,因此 Parallel Old 收集器适用场景也是关注吞吐量的场景。

CMS 收集器

前面我们讲解的所有收集器要么是串型的,比如 Serial 收集器,Serial Old 收集器,要么是并行的,比如 ParNew 收集器、Parallel Old 收集器 以及 Parallel Scavenge 收集器。那么 CMS 收集器是一款串行收集器。

CMS 英文全称 Concurrent Mark Sweep ,从名称我们就可以分析出来,这是一个并发的收集器,并且它使用的是标记-清除算法。CMS 收集器比前面讲解的垃圾收集器要复杂很多,它的执行过程大致如下图所示:

第一阶段,初始标记(initial mark)。初始标记用来标记跟根对象能够直接关联到的对象,那么这个阶段会 Stop The World 。不过由于只会标记根对象直接关联到的对象,所以标记的对象相对比较少,停顿的时间还是比较短的。

第二阶段,初始标记完成之后,又会进入并发标记这个阶段,并发标记这个阶段会找到根对象能够关联到的所有对象。在这个阶段下,垃圾收集线程和用户线程并发执行,所以没有 Stop The World。

第三阶段,并发标记完成之后,又会进入一个不一定会执行的阶段,叫做并发预清理。并发预清理会重新标记那些在并发标记阶段引用被更新的对象,比如晋升到老年代的对象,或者原先就在老年代的对象,从而去减少后面重新标记阶段的工作量。这个阶段也是并发执行的,没有 Stop The World,你也可以使用这个参数:-XX:CMSPrecleaningEnabled 去关闭掉这个阶段,默认情况下是打开的。

第四阶段,经过并发预期清理之后,又会进入一个可能会执行也可能不会执行的阶段,叫做并发可终止的预清理阶段。这个阶段做的事情和并发预清理做的事情是一样的,也是为了减少后面重新标记阶段的工作量去设计的,并且没有 Stop The World 状态。并发可终止的预清理阶段要想执行是有前提的,那就是 Eden 的使用量要大于 CMSScheduleRemarkEdenSizeThreshold 阈值,默认是 2M,否则这个阶段会直接跳过。

那么既然已经有了并发预清理的阶段,为什么 CMS 还要再设计一个并发可终止的预清理的阶段呢?我们来看一下并发可终止的预清理阶段它的主要作用是什么?它的主要作用是允许我们能够控制预清理阶段的结束时机,比如你可以控制扫描多长时间就停止这个阶段,可以使用这个参数:-XX:CMSMaxAbortablePrecleanTime 去控制,默认阈值是 5s,或者控制 Eden 的使用比例,达到一定的阈值就结束这个阶段,可以让我们更好的控制 CMS 收集器的行为,可以使用这个参数:CMSScheduleRemarkEdenPenetration 去设置,默认阈值是 50%。

第五阶段,经过并发预清理以及并发可重的预清理,这两个可能会执行也可能不会执行的阶段之后,CMS 会进入重新标记阶段。为什么前面已经有了并发标记还要再弄一个重新标记呢?

这是因为并发标记阶段的标记过程是并发执行的,所以在标记的同时,用户线程可能会修改已经标记过的对象的状态,这样就可能会导致两种情况:

一是,把已经死亡的对象错误的标记成了存活,那这种情况会导致部分垃圾不被回收,但是还是可以容忍的,因为不影响业务。

二是,是把存活的对象错误地标记成了死亡,那问题就大了,程序都没有办法正常执行了,所以 CMS 设计了重新标记这个阶段,用来修正并发标记期间因为用户限制继续执行而导致标记发生变动的那些对象的标记。

那么一般来说啊,重新标记所花费的时间会比初始标记花费的时间要长一些,但是比并发标记的时间会比初始标记花费的时间要长一些。但是也不是绝对的,主要还是看业务场景。另外,重新标记是存在 Stop The World 的。

第六阶段,再之后,CMS 又到了并发清理阶段,也有的书上翻译成并发清除,这个阶段会基于标记结果清除掉前面标记出来的垃圾,这个阶段也是并发执行的,没有停顿。
 
直接清楚对象是会存在内存碎片的,对吧?那么为什么 CMS 不整理一下对象呢?这是因为并发清除阶段它是并发执行的,如果整理对象的话是要移动对象的位置的,我们很难在并发状态下,一堆的线程在运行我们的业务,一堆的线程在做垃圾回收,回收的同时还要移动对象,然后移动对象位置的同时还要保证应用程序运行不出问题,而且实现起来非常难。所以采用并发清除,而不是并发整理。

最后阶段,最后一个阶段是并发重置,并发重置用来清理清理本次 CMS 的上下文信息,然后为下一次垃圾回收做准备。
 
下面来分析一下 CMS 收集器的特点:

CMS 的优点:一是, Stop The World 的时间比较短,从上图我们可以看出来,只有初始标记以及重新标记这两个阶段存在 Stop The World,其他的阶段都是并发执行的。
 
二是,CMS 大多数过程都是并发执行的。

CMS 的缺点有哪些呢?我们一起来看一下:

一是,CMS 对 CPU 资源是比较敏感的,在并发执行的那几个阶段,虽然不会导致 Stop The World,用户线程不会停顿,但是由于垃圾收集线程也会占用一部分 CPU 资源,所以会影响到应用程序的执行效率,导致吞吐量的降低。因为你想,有一堆的线程在做垃圾回收,同时还有一堆的线程在做业务。那就可能会存在业务线程和垃圾回收线程去争抢 CPU 时间片的情况,导致你的业务线程执行效率下降。

二是,CMS 没有办法处理浮动垃圾,由于 CMS 的并发清除阶段,用户线程还在运行,所以自然会有新的垃圾产生,那这部分垃圾就叫做浮动垃圾。CMS 没有办法在这一次收集就清理掉这些垃圾,要到下一次才能清理掉。

三是,CMS 不能等到老年代几乎满了才开始收集。这是因为在垃圾收集阶段,用户现在也在运行,那要运行就要去申请内存,于是我们必须预留足够的内存给用户线程去使用。要是 CMS 在运行期间预留的内存不能满足应用程序的需要,就会出现一次 Concurrent Mode Failure 这样的异常。这个时候就会导致虚拟机使用 Serial Old 作为后备去收集老年代的垃圾,这就是前面讲 Serial Old 的时候,所谓的 CMS 收集器出现故障的时候,也会使用 Serial Old 作为后备的场景。而一旦使用 Serial Old 的作为后备的话,Stop The World 的时间往往又会比较长了。因此在实际项目中,一定要预留好足够的内存,你可以使用这个参数:CMSInitiatingOccupancyFraction 去配置老年代的使用率达到多少的时候就触发垃圾收集,默认阈值是 68%。

另外 CMS 还存在内存碎片的问题,这个很好理解,因为 CMS 是基于标记清-除算法去实现的,所以会导致内存碎片的产生,这也是 CMS 最被人诟病的地方。当然了,你也可以使用这个参数:UseCMSCompactAtFullCollection 去设置,在完成 Full GC 之后进行内存整理,默认是开启的,也可以使用这个参数:CMSFullGCsBeforeCompaction 去指定每发生 Full GC 几次之后就做一下内存整理,默认阈值是 0。

那么前面我们说过,很多情况下, Major GC 和 Full GC 谈的是一件事情,对吧?但是对于 CMS 来说,Major GC 和 Full GC 不是一回事。CMS 是作用在老年代的垃圾回收,并不是 Full GC。

下面来看一下 CMS 收集器的适用场景:

如果你希望你的系统停顿时间要比较短,响应速度要比较的快,那么就可以考虑使用 CMS。那么一般来说,对于运行在服务器上的各种应用程序,比如一些 Web 应用,使用 CMS 收集器是比较合适的。

最后考虑到 CMS 比较复杂,简单小结一下。我们知道完整的 CMS 收集器总共分成了七个阶段:第一段段是初始标记,第二阶段是并发标记,第三阶段是并发预清理,第四阶段是并发可终止的预清理阶段,第五阶段是重新标记,第六阶段是并发清理,第七阶段是并发重置。其中三和四两个阶段不一定会执行,而且市面上很多文章甚至是直接略过的,因此对于应付面试的话,你只要记得一、二、五、六以及七这五个阶段就足以。不过要想真正理解 CMS,并且能够定位 CMS 垃圾收集器相关的问题,那么这七个阶段还是需要了解的。

以上是关于看看你是如何把握老年代收集器的主要内容,如果未能解决你的问题,请参考以下文章

大对象直接进入老年代

对象如何进入老年代的问题

jvm如何gc,新生代,老年代,持久代,都存储哪些东西

Java 垃圾回收之老年代垃圾回收器

深入浅出CMS垃圾收集器

深入浅出CMS垃圾收集器