上一节我们讲解分代和垃圾回收算法,这一节我们来讲解老年代重要的垃圾收集器:cms收集器。这一节的内容同样比较多。
这一节主要围绕着十分常用的CMS垃圾收集器进行讲解。
上一篇文章我们讲解分代的基础理论,同时讲解了新生代和老年代各自的算法复制算法和标记整理算法,之后我们总结了新生代进入老年代的条件,在最后我们介绍的引用类型,同时进行了练习的提问和相关的解答。
作为最常用的新生代垃圾收集器ParNew,他和cms收集器的搭配在Jdk1.9之前是jdk官方推荐的配置,也是目前最常被用到的收集器组合。
ParNew收集器本身是Serial收集器的多线程版本。而Serial 收集器和Serial Old收集器因为过于古老这里不再进行介绍,但是「并不是说他们已经退出了历史舞台」,文章后面的内容将会提到Serial收集器的关键作用。
最后,需要注意ParNew是除了Serial之外「唯一」可以和cms配合的垃圾收集器
通常情况下,如果是服务端通常更加建议使用多线程收集器,而客户端则更加倾向使用单线程的收集器。因为如果是单核的机器使用多线程会带来额外的“上下文切换”的操作,性能不会提升反而会下降。同时客户端多数情况下对于多线程的要求并不是很高,所以客户端更加推荐使用单线程。
和上面的问题一样,要根据使用的机器是多核还是单核来决定。当然多数情况下会使用多线程,因为现代处理器的多线程技术已经十分成熟。
❝分析: 解答上面的问题首先我们要弄清楚什么是客户端模式,什么是服务端模式,客户端模式中,「-client」 代表了客户端的所需参数,而 「-server」 则是服务器需要的运行参数。 服务端模式:通常适用于多核心的环境,比如对于多线程垃圾回收具备高效利用的Parnew。

客户端模式:如果是单核性能较差的机器适合使用,因为客户端模式通常运行单核,适合Serial收集器,因为他是单线程的,没有线程切换的开销

❞
jdk9之前老年代最常使用的垃圾回收器,主要使用标记-清除算法(不完全使用标记清除算法)。为了保证运行的效率,cms会采用用户线程以及垃圾收集线程并发执行的方式进行处理,也是首款支持用户线程和垃圾回收线程并发的垃圾收集器。
❝之前的文章讨论过,标记清除算法会产生大量的内存碎片,为什么还要使用标记-清除算法呢? 其实cms会根据一个系统参数判定多少次垃圾回收之后执行整理动作,而这个动作需要停下当前所有的用户线程,并且开启单线程Serial收集器对于老年代的内存碎片进行整理,而这里的整理就是使用的标记-整理。 但是通常情况下cms使用的还是标记-清除的动作。 ❞
cms的四个回收步骤比较好理解,主要为四个步骤:
从上面的步骤描述可以看到,cms的垃圾收集器已经有了很大的进步,可以实现并发的标记和并发的整理阶段做到和用户线程并发执行(但是比较吃系统资源),不干扰用户线程的对象分配操作,但是需要注意初始标记和重新标记阶段依然需要「停顿」。
初始标记阶段:需要暂停用户线程, 开启垃圾收集线程, 但是仅仅是收集当前老年代的GC ROOT对象,整个运行过程的速度非常快,用户几乎感知不到。
这里需要注意的是哪些对象会作为GC ROOT,而哪些则不会,比如实例变量不是GC ROOT的对象,同时在根节点枚举当中如果发现没有被引用也会标记为垃圾对象。

cms初始标记
❝哪些节点可以作为gc root
总结:「当有方法局部变量引用或者类的静态变量引用,就不会被垃圾线程回收。」 ❞
并发标记阶段:可以和用户线程一起并发执行,此时系统进程会不断往虚拟机中分配对象,而垃圾收集线程则会根据gc root对于老年代中的对象进行有效性检测,将对象标记为存活对象或者垃圾对象,这个阶段是最为「耗时」的,但是由于是和用户线程并发执行,影响不是很大。
注意这个这个阶段并不能完成标记出需要垃圾回收的对象,因为此时可能存在存活对象变为垃圾对象,而垃圾对象也可能变为存活对象。

cms并发标记
❝补充 - 并发关系和并行关系在jvm的区别: 并行:指的是多条垃圾收集线程之间的关系 并发:垃圾收集器和用户线程之间的关系 ❞
重新标记阶段:这个阶段同样需要stop world,作用是会继续完成上一个阶段的动作,其实是对第二个阶段已经标记的对象再次进行对象是否存活的标记和判断,这个过程是十分快的,因为是对上一个步骤的扫尾工作。

并发清理阶段:这个阶段同样是和用户线程并发执行的,此时用户线程可以继续分配对象,而垃圾回收线程则进行垃圾的回收动作,这个阶段也是比较耗时的,但是由于是并发执行所以影响不是很大。

并发清理
因为在并发标记和并发清理这两个阶段是需要和用户线程并发的,此时需要占用整个系统一部分的资源,留给垃圾线程并发处理使用。
这里还有一个明显的问题就是如果是单核心单线程的系统,cms内部会使用抢占式多任务模拟多核并行的技术,并且开启「增量式收集器」实现线程方式的处理。(意思就是伪双核实现 i-cms的并发处理)但是这个收集器 i-cms 的效果不尽人意,在「jdk7」当中被废弃,在「jdk9」当中已经被完全删除。
单核心单线程的机器需要谨慎考虑是否使用CMS。
简单理解:简单理解就是cms是一个勤快的小伙子,平时有条不紊的进行垃圾回收的操作,但是当垃圾过多小伙子顶不住的时候,此时背后注视一切的老者Serrial收集器大喊一声:stop the world,并且快速进行垃圾回收动作,一切工作完成退隐幕后,让小伙子继续上班。
当然上面的案例不是个人创造,个人学习的时候看到一个非常形象的比喻,当然我们解释的时候肯定不能这么解释,这不是专业人员该说的话。
在用户线程和垃圾回收线程并发运行的同时,因为第二步和第四步是同时运行的,如果此时让老年代满了之后再回收,肯定是不行的,如果此时垃圾线程和用户线程一起工作,会导致用户线程分配内存大于老年代引发OOM的问题。所以cms默认会根据之前介绍的cms参数 「-xx:cmsInitiatingOccupactAtFullCollection」来指定老年代内存占用多少之后进行垃圾回收的动作。
Jdk 中 「-xx:cmsInitiatingOccupactAtFullCollection」参数在 jdk5 是68% ,而jdk6 调整为 92%。
这里还有一个问题,就是如果在并发清理的阶段如果用户线程分配的对象超过剩下的内存(比如最后8%的空间),而此时垃圾回收线程又在工作,那么此时会发出现 「Concurrent Mode Fail ** 的问题,此时会立刻stop world 暂停用户进程并且开启」Serial收集器**进行垃圾回收清理的操作。当垃圾回收完成之后,会开启用户用户线程并且恢复cms收集器的工作。
在实际使用过程中需要小心调整此比例,防止并发失败问题发生。
❝可以看到Serial收集器作为兜底的操作,有人会有疑问为什么兜底用Serial这种单线程垃圾收集器而不用其他的垃圾收集器。 这个问题其实很好回答,类似于redis一样,单线程不一定意味着性能差,多线程也不也意味着性能好,Serial作为老牌垃圾收集器虽然实现很简单,但是具备一个其他收集器没有的优点,就是「效率高,性能好」。所以这也是会为什么使用Serial作为兜底而不是使用其他垃圾收集器。 ❞
这个问题是由于cms本身使用「标记-清除」算法实现而产生,并发标记和并发清理阶段都是对于垃圾对象的直接标记和回收处理,在重新标记阶段也仅仅是对gc root已经标记的对象再进行一次判断而已,所有的过程都不会产生对象的移动操作,这就导致了内存对象是东一块西一块的,如果此时新生代出现大对象要进来,很容易造成频繁 full gc。
官方的解决办法是在每次标记整理结束之后,就对内存进行一次“标记 - 整理”的动作,此时同样需要 “stop world”暂停用户线程,同时将存活对象移动到一处,并且清理掉所有垃圾对象。
Jdk提供了:「-xx:cmsFullGcBefore-Compaction」 参数用于指定多少次full gc 进行一次内存整理,默认是 0 次,表示每次都进行整理操作。
这个点已经提了不知道多少次了,这里再次提一下,同时增加了一条使用CMS收集器的情况下触发老年代Full GC的时机。
这一思考题主要从算法和对象多少两个方面入手,新生代的复制算法和老年代的标记-整理算法所需要的时间开销不一样,同时老年代本身对象过多,同时结合jvm主要采用根节点枚举的特点,必然会导致用户线程的暂停和等待,即使是最新一代收集器(ZGC和Shenadash)可以做几乎完全和用户线程并发,在根节点枚举这一步骤上还是需要暂停用户线程。由此可见,老年代回收速度慢并且我们需要竭力避免老年代触发垃圾回收。
这里再重新强调方法区的回收标准:
1、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2、加载该类的 ClassLoader已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
来自 https://www.cnblogs.com/erma0-007/p/8669318.html
垃圾收集器的细节比较多,所以这篇文章很长,cms垃圾收集器是十分重要并且值得关注的一款收集器。
从这一节可以看到老年代的回收对于cms的副作用十分大,所以下一节将会根据一个模拟的案例讲解规避老年代回收的一些思路。