JVM11:垃圾收集器的并发和并行,Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old,源码分析CMS两种模式,CMS如何定位可达对象?

JVM垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,新生代和老年代的垃圾收集器需要配合使用,jdk1.8的垃圾收集器如下图:
在这里插入图片描述

  • Serial:jdk1.3之前是当时虚拟机新生代的唯一选择
  • Serial Old:Serial收集器的老年代版本
  • ParNew:理解为是Serial收集器的一个多线程版本
  • Parallel Scavenge:看上去和ParNew一样,但是更关注系统的吞吐量,它也叫做吞吐量优先垃圾收集器,jdk1.8默认收集器
  • Parallel Old:Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更关注系统的吞吐量,jdk1.8默认收集器
  • CMS:jdk1.5的一种以获取最短回收停顿时间为目标的收集器,CMS并不算我们常用的并发类垃圾收集器的主流
  • G1:G First(garbage first:垃圾优先),俗称G1。Parallel系列收集器和CMS收集器二者取长补短,关注停顿时间,又能关注吞吐量,它对于内存进行了一次完整的重定义,使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别

垃圾收集器的并发和并行

垃圾收集器也分为并发和并行,这个和我们并发编程中说的并发并行是完全不一样的,要注意区分

  • 并行类收集器:指的是垃圾收集线程有多条线程在一起工作,并且中间不穿插应用线程
  • 并发类收集器:指的是垃圾收集线程能够和用户线程一起跑,或者说中间有某些步骤能够一起跑

如果硬件环境就是单线程,而我们用到的逻辑是并发类的逻辑,它还是只会先跑一个线程,那么这个时候就叫做并发不并行

  • 垃圾收集线程其实会结合硬件条件来选择是并发并行的状态还是并发不并行的状态
  • 如果既要并发,又要并行,那么就开多线程,并使用并发收集器就可以了
  • 所谓的并行其实强依赖于多线程

Serial

Serial收集器是最基本的收集器,也是发展历史最悠久的收集器。在jdk1.3之前Serial是当时虚拟机新生代的唯一选择

它是一种单线程的收集器,因为是单线程所以业务线程没有可能和GC线程一起跑,因此是二选一来回切换,也就是说它在进行垃圾收集的时候需要暂停其他线程(即STW,停止用户线程)。之所以设计成这样,也是一种无奈之举。因为JDK 1.2是1998年12月8日发布,而JDK 1.3是2000年5月8日发布的,可想而知,在那个计算机都不普及的年代,一个CPU上安两个运算核心,啥家庭呀?而且,当时的Java是为了嵌入式设备,以及一些电器去使用(比如电饭煲,按了煮饭按键之后隔1s再去煮饭,也是可以被用户容忍的),并没有考虑到会用到web上。

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

Serial Old

Serial Old收集器是Serial收集器的老年代版本(位于上图的Tenured),也是一个单线程收集器,不同的是采用“标记-整理算法”,运行过程和Serial收集器一样。

Serial收集器工作线程关系图示

请添加图片描述

ParNew

随着时代的发展,更多的web场景的出现,发现Java很优秀,可以拿来作web,因此Java也在逐渐向这方面优化,到了零几年,CPU开始向多核发展,对应的CPU核心数也在增加(所谓CPU的核心数,就是指单个CPU的运算核心的个数。比如拿两个4核拼起来的,叫伪8核,并不算真正意义上的8核)。那么垃圾收集器也是一样,为了更好利用CPU多核的优势,在垃圾收集的时候,采用多个垃圾收集线程去进行收集,这样可以缩短STW的时间。

ParNew收集器是可以理解为是Serial收集器的一个多线程版本,除了使用多线程进行垃圾收集以外,剩下的和Serial收集器一模一样,它们之间复用了大量的的代码,因此在垃圾收集的过程中也要进行STW,但是因为采用了多线程,可以缩短STW的时间,也就是说缩短了业务线程的停顿时间。虽然还是有不足,但是比之单线程会好很多,因为CPU的性能上去了。

优点在多核CPU时,比Serial效率高。如果是单核CPU的情况下,ParNew收集器是绝对比不过Serial收集器的,因为硬件设备只支持单线程,而指定为多线程垃圾收集时,势必会导致线程上下文的切换,从而造成额外的开销。而且,在双核的情况下,ParNew收集器也不能保证会超越Serial收集器,只有在三核、四核的情况下它才能够保证超越Serial收集器。随着CPU的个数以及CPU核心数的增加,ParNew GC可以更好的压榨CPu的性能,它默认开启的垃圾收集线程是和CPU的核心数量相同的,比如32核CPU默认就是开启32个线程,并且在系统环境下,可以用一个参数-XX:ParallelGCThreads来控制GC线程数。这个参数是根据服务器环境去进行配置的。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

ParNew收集器日常使用还是很多的,从图上可以看到,支持CMS的只有Serial收集器和ParNew收集器。

新生代垃圾收集器对于吞吐量的思考

系统一天内要干的事情是可以知道的,那么如何提升单位时间内请求并响应的数量就是我们调优该做的事情。换句话说就是:吞吐量如何能更大一点?

吞吐量大意味着单位时间内可以干更多的事情,吞吐量的计算公式如下:

公式1:吞吐量=并发数/平均响应时间。
公式2:吞吐量=请求总数/总时长。
系统一天内要干的事情是可以知道的,因此可以认为硬件情况一定的情况下,请求数固定,运行用户代码的时间也是固定的,也就是说请求数/运行用户代码的时间就是一个常量,因此可以推导出

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)

比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务,这是理论上的,实际上呢,垃圾收集时间设置的短,它是需要牺牲吞吐量以及新生代空间为代价的,因为时间太短收不完,所以要牺牲空间换时间,比如:
设置停顿时间100ms,100ms只能收100M垃圾,那么就先收100M,收不完下次再收,现在改为50ms,更收不完,这样会导致GC更加频繁,原先可能10s收集一次,停顿100ms,现在5s收集一次,停顿70ms,虽然实际上停顿时间在下降,但是随着GC更加频繁吞吐量也在下降。

因此需要在吞吐量和停顿时间(垃圾收集时间)二者中进行取舍,调优往往就是判断当前场景下我们更愿意舍弃掉哪些东西,来寻找二者之间的一个最优解

  • 停顿时间短的(个人认为适合CPU型服务器):适合在与用户交互的这种交互式应用上,需要一个快速的反馈速度来提升用户体验,因此必然要选择停顿时间短的,停顿时间段时间短意味着垃圾可能收不完
  • 吞吐量大的(个人认为适合内存型服务器):用户代码可以充分利用CPU资源,尽快完成程序的运算任务,停顿时间长意味着可以更加专心的进行垃圾回收,因此不会浪费内存,适合计算型的应用,而且交互不那么频繁的

ParNew是在Serial的前提下更加关注停顿时间的垃圾收集器。那么能不能再关注下吞吐量呢?最好可以控制吞吐量可大可小的收集器呢?
那么有需求就是有落地,于是又款新生代的垃圾收集器横空出世——Parallel Scavenge

Parallel Scavenge

Parallel Scavenge收集器是一个新生代收集器,它既是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量,它也叫做吞吐量优先垃圾收集器。

Parallel Scavenge它提供了两个参数用来精准的控制吞吐量:

  • -XX:MaxGCPasueMillis设置最大的垃圾收集停顿时间,允许的值是>0的毫秒数,会尽可能保证停顿时间<这个值,它是以牺牲吞吐量以及新生代空间,因为时间太短收不完,所以要牺牲空间换时间。比如:
    设置停顿时间100ms,100ms只能收100M垃圾,那么就先收100M,收不完下次再收,现在改为50ms,更收不完,这样会导致GC更加频繁,原先可能10s收集一次,停顿100ms,现在5s收集一次,停顿70ms,虽然实际上停顿时间在下降,但是随着GC更加频繁吞吐量也在下降。
  • -XX:GCTimeRatio用于调整垃圾回收(GC)的时间比例,它指定了在应用程序执行时间中,JVM允许用于GC的时间的比例。通过调整这个参数,可以在应用程序和GC之间进行更好的平衡,以达到更高的吞吐量或更低的延迟。
    • 默认值是99,表示允许99%的时间用于应用程序执行,而1%的时间用于GC。
    • 参数的值并不代表百分比。它用来设置垃圾回收时间与应用程序时间比例的参数,比如设置19,则垃圾回收时间最多占总运行时间的1/19,也就是100分钟有5分钟在收垃圾。

还有一个特别重要的参数,是用户自适应大小策略:
-XX:+UseAdaptiveSizePolicy :它是一个布尔类型的参数,这个参数打开,就不需要指定新生代大小了,包括与Eden与Survior的比值,以及晋升老年代的一些细节参数,它会根据系统当前的默认情况会监控系统信息,动态的调整这些参数。

如果不是对-XX:MaxGCPasueMillis-XX:GCTimeRatio很熟悉的情况下,-XX:+UseAdaptiveSizePolicy 配置的参数可能比你定义的要好,自适应策略也是Parallel Scavenge和ParNew的一个很大的区别之一。

在互联网行业当中,哪怕是降低一点吞吐量,也会让应用和用户的交互尽可能的快。停顿时间尽可能地小,最好是小到一个网络延迟之内。

Parallel Old

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更关注系统的吞吐量

如果应用是那种对于CPU资源非常敏感的,那么可以优先考虑Parallel Scavenge和Parallel Old的组合(jdk1.8默认使用的就是这样的组合)。
在这里插入图片描述

并发类的垃圾收集器的思考

随着业务的发展,还想进一步的将垃圾收集时间缩短,最好短到一次网络延迟之内,或者用户感觉不到,那么最好是用户线程和垃圾收集线程一起跑。我们将业务线程以及垃圾收集器线程一起跑的过程称之为一个并发类的垃圾收集器。显然ParNew以及Parallel Scavenge和Parallel Old已经满足不了hotsport的开发者了,所以要是能够并发收取的话,那么停顿时间就可以进一步降低,哪怕有点降低效率。

并发收取,那依赖关系发生变化怎么办?所以这个时候业务线程和GC线程一起跑,就会出现问题,这个时候如何能避免掉这个问题?
原来完整的收集线程不能根业务线程一起跑,那么是否可以将这个收集过程拆分成多个步骤,同时判断这几个步骤,哪个步骤是可以和业务线程一起跑的,然后将可以一起跑的代码拆分出来,让它和业务线程一起跑。最花时间的那些步骤,让它和业务线程一起去跑,这样在某种程度上能够对停顿时间进一步的缩减
请添加图片描述
于是,就有一款垃圾收集器应运而生——CMS。CMS是在jdk1.5期间hotsport推出的一款在强交互应用中几乎跨时代的一款产品,这款收集器也是hotsport首个真正意义上带并发的垃圾收集器,CMS实现了第一次让垃圾收集线程以及我们用户线程同时工作,默认ParNew收集器是CMS的新生代垃圾收集器,当然也可以通过参数配置成Serial收集器。

CMS

官网 : https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#co
ncurrent_mark_sweep_cms_collector

CMS(Concurrent Mark Sweep:中文翻译过来是同步标记扫描)收集器是一种以获取最短回收停顿时间为目标的收集器,CMS并不算我们常用的并发类垃圾收集器的主流。采用的是标记-清除算法,整个过程分为4步:

  1. 初始标记 CMS initial mark: 标记GC Roots直接关联对象,不用Tracing(跟踪),速度很快
    • 这个过程是需要STW的,因为如果业务线程不停掉,会一直产生GC roots对象
    • 直接关联对象:是指GC roots到堆中引用链上的首个对象,至于引用链上的其它对象先不去操作,假如一个引用链上挂了30多个对象,那么操作起来会非常耗时,所以这里只操作首个对象
    • jdk1.7之前初始标记是串行的,是单线程;jdk1.8之后初始标记是并行的,是多线程。非要讲个理由的话就是jdk总归要做一些版本更新。可以使用参数来修改设置:-XX:+CMSParallelInitialMarkEnabled,开启表示采用并行,关闭表示采用串行,jdk1.8默认是开启的
  2. 并发标记 CMS concurrent mark: 进行GC Roots Tracing
    • 将GC roots直接关联对象的后面一长串对象都找出来,因为不确定引用链上有多少个对象,假如一个引用链上挂了30多个对象,那么操作起来会非常耗时,所以使用并发标记这样的方式,而并发标记的过程中,可能还会产生垃圾,也可能产生新的GC roots
  3. 重新标记 CMS remark: 修改并发标记因用户程序变动的内容
    • 这个过程是需要STW的,因为如果业务线程不停掉,会一直产生GC roots对象
    • 针对第2步的并发标记阶段,产生的垃圾、产生的新的GC roots进行重新标记,这些增量的东西并不会太多,因此耗时不会太长,即使再慢,也不会慢过前面应用线程产生的那么多垃圾
  4. 并发清除 CMS concurrent sweep: 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾

请添加图片描述
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

源码分析CMS两种模式

这里用的是open jdk 1.8 60版本源码,文章顶部下载或者云盘链接

background模式

网上大部分资料都说CMS是标记-清除算法,实际上上面这个图也只是CMS其中一种模式之一,包括标记-清除算法这种表述也是不准确的,因为上面的流程也只是常规状态下的一个主流程而已,不能以偏概全。

这个常规模式,在源码当中也是有名字的,叫做background模式,background模式为正常模式,源码文件是src\share\vm\gc_implementation\concurrentMarkSweep\concurrentMarkSweepGeneration.cpp
在这里插入图片描述
由流程图可知,CMS并不是完全并发的垃圾收集器,包括常说的G First(也就是G1),以及jdk11出现的Z GC,都没有做到所谓的一个完全的并发。市面上的垃圾收集器只有Azul的c4是完全并发的。我们可以得出一个结论,我们平时大部分接触到的的仅仅只是一个常规模式。

CMS并发模式失败问题

并发类的垃圾收集器,它也会并发的产生垃圾。因此会有新的问题:
老年代在并发处理的阶段,业务线程也在不断产生垃圾,这个时候由于还处于并发阶段,还可以往里面扔垃圾,那么这个过程中间老年代可能会满掉,就会导致OOM。
垃圾收集器的开发这也考虑到这个问题,不可能让它OOM,那么这个时候,会将应用暂停,也就是说应用程序会被STW,应用程序STW之后,就开始进行垃圾回收,回收掉这些垃圾之后,再将应用程序恢复,开始继续并行并发的的走。这种无法完成所谓的收集情况的叫做并发模式失败

发生这种情况意味着参数设置错了,包括你的服务器,以及垃圾收集器,以及要回收的应用程序就不匹配。一旦出现并发模式失败,就要开始找问题。

为了避免所谓的并发模式失败问题的产生,这个时候我们希望有一个参数,能够在空间到达一定阈值的时候,就开始触发回收操作,不会真正等到满了的时候再去回收,比如80%的时候,就开始进行全局停顿,执行CMS的另外一个模式——foreground模式。

foreground模式

CMS一共有两种大的模式,除了background模式,还有foreground模式为,它是Full GC模式。
在这里插入图片描述
这个时候,包括在这个模式中,可能会发生并发失败,并且还会出现切换其它垃圾收集器的一个情况的产生,比如切换到Serial Old收集器。
如果说使用过CMS的话,就知道在配置的时候,有两个重要的参数:

  • -XX:CMSInitiatingOccupancyFraction:用来设定回收阈值的,按百分比回收,取值0-100,其他值都表示不生效,默认-1,也就是默认时不开启的
  • -XX:+UseCMSInitiatingOccupancyOnly:仅仅只是一个辅助参数,辅助CMSInitiatingOccupancyFraction参数,如果不设置开启,那么CMSInitiatingOccupancyFraction只会使用一次就恢复自动调整,也就是说是为了让CMSInitiatingOccupancyFraction持久化生效的一个参数

就算设置了 -XX:CMSInitiatingOccupancyFraction参数,比如设置回收阈值80%,那么也不会在80%的时候立刻开始回收,因为这只是满足了条件而已,而CMS什么时候触发GC呢?
CMS也会有一个后台的扫描线程来进行决定,这个扫描线程2s/次,也就是说,有2s的这么一个时间阶段,如果说达到条件在扫描线程的前1s,那么它会在后1s采取执行CMS GC。

那么这两个参数的值什么时候生效呢?
可以去看下jdk对应版本的hotsport的一个源码,搜索CMSInitiatingOccupancyFraction参数,源码文件是src\share\vm\runtime\globals.hpp,这个文件是定义一些默认值的;
在这里插入图片描述
这里可以看到,CMSInitiatingOccupancyFraction默认值是-1,这个参数是一个0~100的百分比范围。
如果不在这个范围内,那么会使用CMSTriggerRatio这个参数,默认值是80,搜索这个参数,可以发现,在src\share\vm\gc_implementation\concurrentMarkSweep\concurrentMarkSweepGeneration.cpp文件中有这么一个方法,CMS初始化空间会走这样的一个逻辑:
在这里插入图片描述
搜索init_initiating_occupancy这个方法
在这里插入图片描述这段代码解释一下,第一个传进来的参数io就是CMS设置的阈值,如果io>=0,就转化为百分比,否则:

((100 - MinHeapFreeRatio) + (double)(tr * MinHeapFreeRatio) / 100.0) / 100.0;

这里有两个参数,tr和MinHeapFreeRatio,它们是干嘛的?
tr我们知道是传进来的参数CMSTriggerRatio,默认值是80,MinHeapFreeRatio的默认值,是在文件src\share\vm\runtime\globals.hpp中定义的,默认值是40。
在这里插入图片描述
MinHeapFreeRatio这个值不同jdk版本不一样,同个jdk的小版本不同也会不一样,建议换算时直接打印JVM参数去看。
在这里插入图片描述
按0的话就是100%,按40换算一下:(100-40)+(80×40)÷100=92%

因此可以知道一般会预留8%的空间,如果并发清理期间,系统要放入老年代的对象超出了空闲空间,这个时候一边收一边放装不下了,那么就意味着并发模式失败,这个时候会导致老年代的内存不足,它就会由background模式转为foreground模式,同时默认会启用Serial Old收集器代替CMS,强制把系统STW,重新再进行一次长时间的GC roots,并且会执行一次Full GC。

注意:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collection和full collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的。

整理算法算法——MSC

Full GC产生的原因,是因为老年代空间不足,或着说内存不连续(因为CMS用的是“标记-清除算法”)导致无法放入新的对象。而foreground模式是一个单纯的Full GC模式的逻辑,会切换到Serial Old收集器,Serial Old是一个单线程老年代收集器,采用“标记-整理算法”,所以说:

foreground在特定的模式下,会用一种整理算法——MSC(mark sweep compact,compact就是压缩的意思),同时会有两个参数对其进行控制:

CMS的空间碎片问题
CMS的空间碎片问题,最终会通过以下参数解决:

//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//默认0 几次Full GC后开始整理
-XX:CMSFullGCsBeforeCompaction=0

这两个参数所做的事情是一种策略,会去尝试着干一件事情。
开启之后,会在几次Full GC之后开始整理内存,也就是进行一次MSC算法的整理,但我们并不希望Full GC发生,因此在某些程度上,通过整理算法整理掉空间碎片这样的方式,来避免CMS的空间碎片。

并发预处理

并发预处理做的工作,还是标记,旨在减少重新标记阶段的工作量,用于在垃圾收集过程中减少停顿时间

并发预处理做的标记和重新标记很像,重新标记是把增量的部分标记出来,并发预处理则是希望能够在重新标记阶段STW之前,能够尽可能地多做一些工作,看下并发标记的这个阶段有没有增量,用很短一个时间将它们标记,从而将增量继续细化,进一步减少重新标记的时间。

很多资料说CMS的工作流程是5步,而不是4步,将并发预处理划分在并发标记之后,在重新标记之前。我们将CMS工作流程划分位4步的方式,是希望将STW的过程凸显出来,而并发预处理实际上属于并发标记流程中后置的一小部分。CMS并没有针对要做几个过程有一个具体的描述,所以在划分的时候,并发预处理可以看成是并发标记的中的一步,可以看成是CMS的一个策略。

因为并发标记这个阶段业务线程也在一起跑,而且周期很长,因此可以在整理完成后,帮重新标记阶段干点活,也标记一些增量,这样可以缩短重新标记的时间,哪怕并发标记这个周期会拖得很长,因为他本来时间就很长,所以再长点我也不在乎。

CMS如何定位可达对象?CMS对新生代的策略

那么这个时候还有一个疑问,如何确定老年代的对象是活着的?
你当然可以回答可达的对象是活着的。但是这个会忽略一些问题,比如:CMS是一个老年代收集器,用CMS去找从GC root出发指向老年代的对象当然可以,那么如果,是GC root的直接引用是指向年轻代的对象,而引用链上有老年代的对象如何识别?换句话说,CMS如何判断新生代的对象为可达对象?
请添加图片描述
CMS如何判断新生代的对象为可达对象?
必须去扫描新生代才能知道,CMS是一个老年代收集器,但是再重新标记的阶段要扫描新生代,也就是全量的扫描几乎整个堆内存,肯定会很慢,而CMS号称是并发类的垃圾收集器,而这个时候却要全量扫描堆,那用户肯定觉得CMS一般。

所以CMS提供了一个能够快速的识别新生代以及老年代活着的对象的一个机制,那么怎么识别最快,当然是将不可达对象直接干掉:在扫描新生代之前,先进行一次mingor GC。

因此有了可中止的并发预处理这个策略

可中止的并发预处理

在扫描新生代之前,先进行一次mingor GC,如果没有mingor GC,就一直等,那么我们希望这个阶段是可以中止的,因此有了可中止的并发预处理策略
CMS控制中止并发预处理的参数:

//默认值2M,表示开始可中止的并发预处理
-XX:CMSScheduleRemarkEdenSizeThreshold
//默认值50%,超过结束可中止的并发预处理,进入重新标记
-XX:CMSScheduleRemarkEdenPenetration

//默认值5s,超过结束可中止的并发预处理,进入重新标记
-XX:CMSSMaxAbortablePrecleanTime
  • 如果Eden区扫描发现超过2M(-XX:CMSScheduleRemarkEdenSizeThreshold),在这个情况下可中止的并发预处理就会启动
  • 并且当Eden的使用率达到50%(-XX:CMSScheduleRemarkEdenPenetration)之后停止,进入重新标记
  • 等待5s(-XX:CMSSMaxAbortablePrecleanTime),到了5s不管发不发生mingor GC都会退出这个状态,进入重新标记
  • 除此之外,在可中止的并发预处理阶段,发生一次mingor GC,进入重新标记

可中止的并发预处理尽可能的承担重新标记的工作,这个阶段会持续的时间会很长,直到干的差不多了再给到重新标记这个流程,它和重新标记是做相同的事情,

那么有个问题就是,可中止的并发预处理执行多长时间,可以保证会发生一次mingor GC?
这个没办法控制,这是JVM自己调度的。

CMS对老年代的策略

老年代会划分为很多个块,每个块512字节,也就是512B,分块是为了分区从而方便进行操作。

那么最好还要有一个数据结构来管理这些块,比如单次遍历就用一个表,来记录每个数据块干了哪些事情,那么这里也有这样的一个表的数据结构,叫做Card Table(卡表),如果在进行并发标记的时候,对象引用发生了变化,重新标记阶段,会标记在并发阶段重新加入的对象。

Card Table

Card Table就是为了解决跨代引用的问题,它通常会有两种方式来记录引用关系,point outpoint inpoint[指,指向;对准]的意思,比如obj1.aaa=obj2;

  • point out:会在obj1对象里面开辟出一小块区域,去记录obj2
  • point in:会在obj2对象里面开辟出一小块区域,去记录obj1

CMS使用的是point out,而G1使用的是point in

Card Table的作用:

  • 当对象引用发生变化的时候,会去标记这个对象,将其标记为dirty card
    • 在进行并发标记的时候,对象引用发生了变化,老年代对象A所在的内存块号为3,它的引用链上新增了一个老年代对象D,这个时候会将A所在的块在表中记录下来,标记为dirty card
    • 在进行并发预处理的时候,会将A的引用链上变化的对象D标记为可达
    • 在进行重新标记的时候,会标记在并发阶段重新加入的对象,在对D进行标记的同时,会清除Card Table里的dirty card标识
  • 标识对象的指向关系,比如是否是老年代指向年轻代
    计算机中用来表示内存储器容量大小的基本单位是字节(Byte)。也就是说,每个所谓的内存块代表的都是一个字节,一个Byte有8位,那么这8位数就可以去约定每一位其中的含义,自然可以使用其中1位来标识对象的指向关系。

CMS中Card Table是怎么玩的?
在CMS中,需要记录的跨代引用会很少,因为年轻代就就划分为两块Eden,Survivor,Card Table会用1字节的数据,去记录512字节的内存是否存在对象指向新生代,1字节二进制是8位,因此可以改变其中一位,比如00000000改为01000000就表示引用存在对象指向新生代,这种标记方法叫做脏标记,意思就是这块数据脏了,表示有做过修改。

相关参数

//开启CMS垃圾收集器
-XX:+UseConcMarkSweepGC
//开启后,CMS初始标记阶段为并行,jdk1.8默认开启
-XX:+CMSParallelInitialMarkEnabled

【CMS两种模式转换(会由background模式转为foreground模式)的阈值设置参数相关】
//用来设定回收阈值的,取值0-100,默认-1,按百分比回收
-XX:CMSInitiatingOccupancyFraction 
//仅仅只是一个辅助参数,辅助CMSInitiatingOccupancyFraction的参数,如果不设置开启,那么CMSInitiatingOccupancyFraction只会使用一次就恢复自动调整,也就是说是为了让CMSInitiatingOccupancyFraction持久化生效的一个参数
-XX:+UseCMSInitiatingOccupancyOnly

【CMS对新生代的策略:可中止的并发预处理参数】
//默认值2M,表示开始可中止的并发预处理
-XX:CMSScheduleRemarkEdenSizeThreshold
//默认值50%,超过结束可中止的并发预处理,进入重新标记
-XX:CMSScheduleRemarkEdenPenetration
//默认值5s,超过结束可中止的并发预处理,进入重新标记
-XX:CMSSMaxAbortablePrecleanTime

【CMS对老年代的策略:Full GC MSC整理算法控制参数】
//这两个参数所做的事情是一种策略,会去尝试着干一件事情,开启之后,会在几次Full GC之后开始整理内存
//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//默认0 几次Full GC后开始整理
-XX:CMSFullGCsBeforeCompaction=0 

CMS的缺陷

优点:并发收集、低停顿
缺点:①由于低停顿,垃圾可能收不完;②CMS用的是“标记-清除算法”,因此会产生大量空间碎片;③并发阶段会降低吞吐量,还会并发失败

  • 在硬件跟不上的情况下,例如单核CPU的服务器,这种单线程情况下效率会很低
    这个是一定的,因为本身并发类的垃圾收集器在线程的需求上就依赖于多线程,所以在单线程环境运行必然会存在一定的缺陷,甚至可能在双核的情况下,都不一定能够比得过前面的Serial Old,Parallel Old收集器
  • CMS还有一个特殊的状态,叫做并发模式失败
    一旦并发失败,所以一旦进入到foreground模式下的时候,有可能我们就会在可终止的预处理这一步等待5s,可终止的预处理默认是5s,所以说,一旦进入foreground模式,灭顶之灾,那么有什么策略或者是方式能够帮助我们来防止这种情况发生吗?因为如果出发FUll GC,无论是否会采用MSC这种整理算法对内存区域进行整理,都已经无法保证并发收集了,这个时候它回去切换到单线程,并且Full GC一旦发生在业务高峰期,或者说是并发收集的高峰期,那么它完全有可能导致你的服务终止几秒,极限情况下甚至十几秒。

服务终止几秒,极限情况下甚至十几秒,这种用户体验,就可能导致项目失败,那么这个时候怎么能够缓解这种情况呢?或者说,我们是否有办法避免发生这种情况的时间是在并发高峰期?

CMS并发模式失败导致服务停止怎么解决?

服务终止几秒,极限情况下甚至十几秒,那么这种情况最好是发生在运营睡觉的时候,这样运营就不能甩锅给我们,同样的,也可以选择用户和应用交互较少的时间,比如大半夜。这样出现问题了我们可以回复一个网络异常,让它过5s后重试,有的大厂就是这样的方式。

另一种方式,就是半夜手动进行Full GC,写个定时脚本去跑,一般2~4点之间去执行,这样尽可能缓解白天的压力,并且执行Full GC的时候,还要手动整理堆,那么怎么整理呢?
将MSC整理算法的这两个参数打开,并且执行System.gc()。

//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//默认0 几次Full GC后开始整理
-XX:CMSFullGCsBeforeCompaction=0

通过这种神不知鬼不觉的方式优化内存碎片,整理堆内存,降低所谓的业务高峰期发生Full GC的概率,当然这种事情可定不能光明正大的做,也不能作为方案提供给业务部门那边,它只是作为技术部门的技术方案进行讨论。很多大公司都是采用的这样的一种策略。

思考

CMS作为jdk1.5有着重要意义的一款垃圾收集器,不可否认它在并发收集上是跨时代的产品,但是它仍然有某些无法解决的问题。

在垃圾回收上我们只有两个点可以关注,调优也是根据自己服务的情况对二者进行取舍:

  • ①停顿时间:CMS是一种以获取最短回收停顿时间为目标的收集器。它将停顿时间尽可能的缩小,不管多小,越小越好,但是作为并发类的垃圾收集器,并没有说可以小到多少,或者说提供一个参数来控制停顿时间
  • ②吞吐量:就吞吐量而言,CMS也不能说是一款非常优秀的垃圾收集器,因为它并没有任何参数用来控制,控制力度非常小,这一点不如Parallel Scavenge,Parallel Old,虽然Parallel系列的这俩是并行的,CMS是并发的

那么jdk的开发者和hotsport的开发者他就想,能不能将CMS并发的特性和Parallel系列的控制手段融合起来,然后G1就应运而生。

汇总

JVM1:官网了解JVM;Java源文件运行过程、javac编译Java源文件、如何阅读.class文件、class文件结构格式说明、 javap反编译字节码文件;类加载机制、class文件加载方式

JVM2:类加载机制、class文件加载方式;类加载的过程:装载、链接、初始化、使用、卸载;类加载器、为什么类加载器要分层?JVM类加载机制的三种方式:全盘负责、父类委托、缓存机制;自定义类加载器

JVM3:图解类装载与运行时数据区,方法区,堆,运行时常量池,常量池分哪些?String s1 = new String创建了几个对象?初识栈帧,栈的特点,Java虚拟机栈,本地方法发栈,对象指向问题

JVM4:Java对象内存布局:对象头、实例数据、对齐填充;JOL查看Java对象信息;小端存储和大端存储,hashcode为什么用大端存储;句柄池访问对象、直接指针访问对象、指针压缩、对齐填充及排序

JVM5:JVM内存模型与运行时数据区的关系,堆为什么分区,分代年龄,Young区划分,Survivor区为什么分为S0和S1,如何理解各种GC:Partial GC、Full GC、Young GC

JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出

JVM7:垃圾回收是什么?从运行时数据区看垃圾回收到底回收哪块区域?垃圾回收如何去回收?垃圾回收策略,引用计数算法及循环引用问题,可达性分析算法

JVM8:引用是什么?强引用,软引用,弱引用,虚引用,ReferenceQueue引用队列;对象生命周期有哪些阶段?创建、应用、不可见、不可达、收集、终结、对象空间重分配;重写finazlie方法弊端

JVM9:STW:stop the world,什么时候会垃圾回收?垃圾收集算法:标记清除算法、标记复制算法、标记整理算法;清除算法的整理顺序:任意顺序,滑动顺序;什么是分代收集算法?

JVM10:JVM参数分类,JVM标准参数,JVM非标准参数,-X参数,-XX参数,其他参数;查看JVM参数,idea控制台输出JVM参数,单位换算;设置JVM参数的常见方式;常用JVM参数及含义

JVM11:垃圾收集器的并发和并行,Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old,源码分析CMS两种模式,CMS如何定位可达对象?

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值