深刻了解JVM - CMS收集器
前言
上一节咱们解说分代和垃圾回收算法,这一节咱们来解说老年代重要的垃圾收集器:cms收集器。这一节的内容同样比拟多。
这一节次要围绕着非常罕用的CMS垃圾收集器进行解说。
前文回顾
上一篇文章咱们解说分代的基础理论,同时解说了新生代和老年代各自的算法复制算法和标记整顿算法,之后咱们总结了新生代进入老年代的条件,在最初咱们介绍的援用类型,同时进行了练习的发问和相干的解答。
概述
- 讲述cms收集器之前,简略理解他的黄金搭档ParNew
- 解说cms收集器的参数,以及外围的运行步骤局部。
- 解说CMS收集器运行过程的一些细节以及CMS的参数的意义。
- 整顿小局部常见的JVM问题
黄金搭档ParNew
作为最罕用的新生代垃圾收集器ParNew,他和cms收集器的搭配在Jdk1.9之前是jdk官网举荐的配置,也是目前最常被用到的收集器组合。
ParNew收集器自身是Serial收集器的多线程版本。而Serial 收集器和Serial Old收集器因为过于古老这里不再进行介绍,然而并不是说他们曾经退出了历史舞台,文章前面的内容将会提到Serial收集器的关键作用。
最初,须要留神ParNew是除了Serial之外惟一能够和cms配合的垃圾收集器
特点:
- 和Serrial只是单线程和多线程区别
- 除了Serrial之外惟一能够和cms配合的垃圾收集器
问题解答:
多线程回收器和单线程回收器那个好?
通常状况下,如果是服务端通常更加倡议应用多线程收集器,而客户端则更加偏向应用单线程的收集器。因为如果是单核的机器应用多线程会带来额定的“上下文切换”的操作,性能不会晋升反而会降落。同时客户端少数状况下对于多线程的要求并不是很高,所以客户端更加举荐应用单线程。
Serial 和 ParNew那个 回收器要好?
和下面的问题一样,要依据应用的机器是多核还是单核来决定。当然少数状况下会应用多线程,因为古代处理器的多线程技术曾经非常成熟。
剖析:
解答下面的问题首先咱们要弄清楚什么是客户端模式,什么是服务端模式,客户端模式中,-client 代表了客户端的所需参数,而 -server 则是服务器须要的运行参数。
服务端模式:通常实用于多外围的环境,比方对于多线程垃圾回收具备高效利用的Parnew。
客户端模式:如果是单核性能较差的机器适宜应用,因为客户端模式通常运行单核,适宜Serial收集器,因为他是单线程的,没有线程切换的开销
CMS回收器
jdk9之前老年代最常应用的垃圾回收器,次要应用标记-革除算法(不齐全应用标记革除算法)。为了保障运行的效率,cms会采纳用户线程以及垃圾收集线程并发执行的形式进行解决,也是首款反对用户线程和垃圾回收线程并发的垃圾收集器。
之前的文章探讨过,标记革除算法会产生大量的内存碎片,为什么还要应用标记-革除算法呢?
其实cms会依据一个零碎参数断定多少次垃圾回收之后执行整顿动作,而这个动作须要停下以后所有的用户线程,并且开启单线程Serial收集器对于老年代的内存碎片进行整顿,而这里的整顿就是应用的标记-整顿。
然而通常状况下cms应用的还是标记-革除的动作。
CMS收集器特点:
- 不能独自应用,须要和其余收集器配合,并且只能和Serrial、ParNew这两个收集器配合
- 为了保障运行的效率,cms会采纳用户线程以及垃圾收集线程并发执行的形式进行解决。也是首款反对用户线程和垃圾回收线程并发的垃圾回收器
- 基于标记-革除的算法。
- 侧重于最短进展工夫的一款垃圾收集器
CMS主要参数:
- -XX:ParallelGCThreads:限度垃圾回收线程的数量,默认状况下线程数量为(cpu外围总数+ 3) / 4,比方8个核的线程为2个垃圾收集线程
- +UseCms-CompactAtFullCollection(jdk9开启废除):开启之后,运行每次FullGc之后内存碎片并且进行整顿的操作,而内存整理须要进行用户线程。会减少整个stop the world的工夫
- -XX:CMSFullGCsBefore-Compaction(jdk9开启废除):留神这个参数失效的前提是+UseCms-CompactAtFullCollection这个参数开启,用于管制多少次FullGc之后进行内存整理,默认是0次,示意每次都进行内存碎片的整顿操作。
- -XX:CmsInitiatingOccupancyFranction:用于限度老年代内存占用超过多少占比之后开启垃圾回收的动作。jdk5为68%,jdk6之后为92%。
CMS的运行步骤(重点)
cms的四个回收步骤比拟好了解,次要为四个步骤:
- 初始标记:这个过程非常疾速,须要 stop the world,遍历所有的对象并且标记初始的gc root
- 并发标记:这个过程能够和用户线程一起并发实现,所以对于零碎过程的影响较小,次要的工作为在零碎线程运行的时候通过gc root对于对象进行根节点枚举的操作,标记对象是否存活,留神这里的标记也是较为迅速和简略的,因为下一步还须要从新标记
- 从新标记:须要 stop the world,这个阶段会持续实现上一个阶段的动作,对于上一个步骤标记的对象进行二次遍历,从新标记是否存活。
- 并发清理:和用户线程一起并发,负责将没有Gc root援用的垃圾对象进行回收。
从下面的步骤形容能够看到,cms的垃圾收集器曾经有了很大的提高,能够实现并发的标记和并发的整顿阶段做到和用户线程并发执行(然而比拟吃系统资源),不烦扰用户线程的对象调配操作,然而须要留神初始标记和从新标记阶段仍然须要进展。
初始标记
初始标记阶段:须要暂停用户线程, 开启垃圾收集线程, 然而仅仅是收集以后老年代的GC ROOT对象,整个运行过程的速度十分快,用户简直感知不到。
这里须要留神的是哪些对象会作为GC ROOT,而哪些则不会,比方实例变量不是GC ROOT的对象,同时在根节点枚举当中如果发现没有被援用也会标记为垃圾对象。
哪些节点能够作为gc root
- 局部变量自身就能够作为GC ROOT
- 动态变量能够看作是Gc Root
- Long类型index的遍历循环会作为GT ROOT
总结:当有办法局部变量援用或者类的动态变量援用,就不会被垃圾线程回收。
并发标记
并发标记阶段:能够和用户线程一起并发执行,此时零碎过程会一直往虚拟机中调配对象,而垃圾收集线程则会依据gc root对于老年代中的对象进行有效性检测,将对象标记为存活对象或者垃圾对象,这个阶段是最为耗时的,然而因为是和用户线程并发执行,影响不是很大。
留神这个这个阶段并不能实现标记出须要垃圾回收的对象,因为此时可能存在存活对象变为垃圾对象,而垃圾对象也可能变为存活对象。
补充 - 并发关系和并行关系在jvm的区别:
并行:指的是多条垃圾收集线程之间的关系
并发:垃圾收集器和用户线程之间的关系
从新标记
从新标记阶段:这个阶段同样须要stop world,作用是会持续实现上一个阶段的动作,其实是对第二个阶段曾经标记的对象再次进行对象是否存活的标记和判断,这个过程是非常快的,因为是对上一个步骤的开头工作。
并发清理
并发清理阶段:这个阶段同样是和用户线程并发执行的,此时用户线程能够持续调配对象,而垃圾回收线程则进行垃圾的回收动作,这个阶段也是比拟耗时的,然而因为是并发执行所以影响不是很大。
cms收集器引发了哪些问题
线程资源被垃圾收集线程占用(cpu资源占用问题)
因为在并发标记和并发清理这两个阶段是须要和用户线程并发的,此时须要占用整个零碎一部分的资源,留给垃圾线程并发解决应用。
这里还有一个显著的问题就是如果是单核心单线程的零碎,cms外部会应用抢占式多任务模仿多核并行的技术,并且开启增量式收集器实现线程形式的解决。(意思就是伪双核实现 i-cms的并发解决)然而这个收集器 i-cms 的成果不尽人意,在jdk7当中被废除,在jdk9当中曾经被齐全删除。
单核心单线程的机器须要审慎思考是否应用CMS。
Concurrent Mode Fail
简略了解:简略了解就是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的机会。
- 老年代可用间断空间小于新生代全副对象的大小
- 老年代可用间断空间小于历次降职老年代的新生代均匀大小
- 新生代内存minor gc无奈进入survior区域, 而老年代空间又有余的时候
- -xx:cmsInitiatingOccupactAtFullCollection 在 Cms垃圾收集器的状况下,如果并发清理阶段对象调配到的大小超过最初8% 的空间大小之后,会触发 concurrent Fail导致失败。
思考题:为什么老年代垃圾回收速度会比新生代慢这么多,到底慢在哪里?
- 首先老年代内存对象十分多,GC ROOT的速度是十分慢的,垃圾回收工夫被拉长。
- 标记-整顿的算法在清理后须要挪动大量的对象到一处,同时须要更新跨代援用以及对象的援用地址等,耗时较长。而新生代复制算法的同时对象都比拟小,算法间接将存活对象拷贝而后清理掉eden区域,最初留下很少的对象进入老年代。
- 如果应用标记革除算法,会导致内存的碎片。如果碎片过多,须要进行线程进行移动和整顿。
这一思考题次要从算法和对象多少两个方面动手,新生代的复制算法和老年代的标记-整顿算法所须要的工夫开销不一样,同时老年代自身对象过多,同时联合jvm次要采纳根节点枚举的特点,必然会导致用户线程的暂停和期待,即便是最新一代收集器(ZGC和Shenadash)能够做简直齐全和用户线程并发,在根节点枚举这一步骤上还是须要暂停用户线程。由此可见,老年代回收速度慢并且咱们须要极力防止老年代触发垃圾回收。
GC回收的“无用的类”(办法区):
这里再从新强调办法区的回收规范:
1、该类所有的实例都曾经被回收,也就是Java堆中不存在该类的任何实例
2、加载该类的 ClassLoader曾经被回收
3、该类对应的java.lang.Class对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法
来自 https://www.cnblogs.com/erma0...
写在最初
垃圾收集器的细节比拟多,所以这篇文章很长,cms垃圾收集器是非常重要并且值得关注的一款收集器。
从这一节能够看到老年代的回收对于cms的副作用非常大,所以下一节将会依据一个模仿的案例解说躲避老年代回收的一些思路。