4. 垃圾回收器

本篇讲述了串行、吞吐量优先、响应时间优先的垃圾回收器,关于自从JDK9 开始默认的 G1 垃圾回收器会在下篇文章讲述。

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核cpu
    • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4
  3. 响应时间优先
    • 多线程
    • 堆内存较大,多核cpu
    • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
接下来我们来学习垃圾回收器,垃圾回收器我们可以把它分为这三类,第一类叫串行的垃圾回收器,第二类叫吞吐量优先的垃圾回收器,第三类叫响应时间优先的垃圾回收器。下面就解释一下,第一个串行的垃圾回收器,这个从名字上也能猜出来,它的底层是一个单线程的垃圾回收器,也就是说,它在垃圾回收发生时,其他的线程都暂停,这时候一个单线程的垃圾回收器就登场了,它一个线程来完成垃圾回收,显然它的适用场景是堆内存较小的时候,而且cpu的核数多了也没用,因为只有一个线程,适合个人电脑这就好比我们有一个居民楼,有一个保洁工人来打扫卫生,一个保洁工人就类似于这种单线程的垃圾回收器,如果楼层比较矮,小三层,那保洁工人可能一天就能把卫生打扫完了,但是如果楼层特别的高,是一个三十几层的高层建筑,那一个保洁工人来打扫,这个工作量可能是非常大的,干好几天都干不完。这是串行垃圾回收器它的适用场景。它适合堆内存小,个人电脑也就是cpu个数少的这么一个工作环境。而下两种吞吐量优先和响应时间优先的这两种垃圾回收器,它们都是多线程的,多线程的好处,还拿刚才那个例子作比喻,这个楼层很高,但是我可以多找几个保洁工人啊,它们每个人打扫一层或者几层,人多力量大,肯定还是可以在规定时间内完成垃圾回收的任务。所以后两种垃圾回收器都适合堆内存较大的场景,并且一般它需要多核的cpu来支持,为什么非要说多核cpu呢,虽然有多个线程,但是假设只有一个cpu,那么工作的时候也是多个线程轮流去争抢这单核cpu的时间片,其实这个效率还不如单线程呢,举个例子,就好比虽然有多个保洁工人来打扫卫生,但是扫帚只有一把,那要打扫卫生必须轮流使用这把扫帚,这个效率显然跟一个人来打扫是一样的,所以后两种垃圾回收器它们的工作适用场景是堆内存很大,但是有一个要求,就是必须是多核cpu才能充分发挥它俩的威力,显然多核cpu都是服务器电脑,所以它们都适合工作在服务器上。后两种吞吐量优先跟响应时间优先它们都是多线程的,那它俩之间又有什么区别呢。我们先说响应时间优先,响应时间优先它就是注重的是让垃圾回收时它的 Stop The World 的时间尽可能的短,我们都知道垃圾回收时它要把其他的线程暂停下来,等打扫完垃圾了,其他线程才能恢复运行,这段时间我们把它叫做 STW,就是世界暂停,这个世界暂停时间显然是越短越好了,那响应时间优先的垃圾回收器它要考虑的就是尽可能的让这个暂停时间变短,尽可能让 STW 的时间最短,这是它的一个目标。那么吞吐量优先呢,它的目标就不太一样,吞吐量优先是指我要在单位时间内让我的垃圾回收所占用的 STW 的时间最短,让单位时间内 STW 的时间最短。吞吐量优先和响应时间优先都是 STW 最短,那么它俩有什么区别呢,这个线程优先它是让单次的 STW 时间最短,举个例子,比如说,单位时间内触发了很多次垃圾回收,每一次垃圾回收都只花了 0.1s,假设1小时内发生了5次垃圾回收,每次都是0.1,那么它们加起来最后等于0.5s,也就是一小时内花费了0.5s,但是每次都很短,这是响应时间优先它的目标。但是吞吐量优先则不同,它可能单次的垃圾回收时间花费的较长,比如说1次花了0.2s,但是在一个小时内它只发生了两次垃圾回收,所以它的总时间来讲只有0.4s,从这一点上来讲,它似乎在总时间上又要优于响应时间优先,所谓的吞吐量就是指垃圾回收的时间占程序运行时间的占比,垃圾回收时间占比越低,那么就指吞吐量越高,这是吞吐量它的一个含义。

下面我们就来具体学习每一种垃圾回收器

4.1 串行

-XX:+UseSerialGC = Serial + SerialOld

第一种垃圾回收器叫串行垃圾回收器,开启串行垃圾回收器的语句VM参数是:-XX:+UseSerialGC = Serial + SerialOld,可以打开串行的垃圾回收器,但是这里要注意,串行垃圾回收器分为两个部分,一个叫 Serial,一个叫 SerialOld,前面这个Serial它是工作在新生代,它采用的回收算法还是复制算法,而 SerialOld 是工作在老年代,它采用的回收算法就不是复制算法了,它采用的是标记+整理算法,整理是不会产生内存碎片,但是效率会低一些,这是开启串行垃圾回收器的JVM参数,包括新生代的和老年代的两个垃圾回收器,其实新生代的和老年代的垃圾回收器是分别运行的,比如说新生代内存不足,使用 Serial 来完成垃圾回收,等到老年代空间不足,这时候才会使用 Serial 来完成新生代的 Minor GC,SerialOld 来完成老年代的 Full GC。这是关于JVM参数。那具体的回收过程呢,我们也来一起看一下。

假设现在有多核cpu,0、1、2、3,一共有四核cpu,刚开始这些线程都在运行,这时候发现堆内存可能不够了,不够了触发了一次垃圾回收,那触发垃圾回收的时候首先要让这些线程在一个安全点停下来,为什么要让它们都停下来呢,前面我们也解释过,因为在垃圾回收的过程中,可能这个对象的地址要发生一个改变,为了保证安全的使用这些对象地址,需要所有用户正在工作的线程都到达这个安全点暂停下来,这时候呢来完成垃圾回收的工作就不会有其他线程来干扰我了,否则的话如果正在移动一个对象,它的地址改了,那其它的线程来找这个对象它就可能找到错误的地址,从而发生问题,当然这里要注意因为我们的 Serial 包括 SerialOld 它们都是单线程的垃圾回收器,因此只有一个垃圾回收线程在运行,在这个垃圾回收线程运行的同时,其他的用户线程都要阻塞,也就是暂停,等待垃圾回收线程的结束,等到垃圾回收线程结束了以后呢,那我们其他的用户线程再恢复运行。好,这就是我们串行垃圾回收器它的一个工作模式。这张图它既适合 Serial,也适合 SerialOld,它们工作的方式都是单线程并且都是要执行 STW 的操作,只不过它们用的回收算法有所不同

4.2 吞吐量优先

  • -XX:+UseParallelGC ~ -XX:+UseParallelOldGC

    这个参数是打开吞吐量优先的垃圾回收器,1.8下默认的垃圾回收器,前面的是新生代的吞吐量优先垃圾回收器,后面的是老年代的吞吐量优先垃圾回收器,只要打开其中一个另一个也会打开,其中新生代的吞吐量优先垃圾回收器采用复制算法,老年代的吞吐量优先垃圾回收器采用标记+整理,并行的垃圾回收器。
  • -XX:UseAdaptiveSizePolicy

    动态的调整伊甸园和幸存区比例、堆大小、晋升阈值等来达到目标,
  • -XX:GCTimeRatio=ratio

    这是一个目标,垃圾回收器会动态调整来达到这个目标1/(1+ratio)是一个计算公式,垃圾回收占用的总时间的比例不能超过这个值,一般会增大堆内存,这样垃圾回收的频率降低,总的垃圾回收时间降低。注意:这个目标指的是总的垃圾回收时间占比
  • -XX:MaxGCPauseMillis=ms

    每一次最大的垃圾回收的时间,一般会减小堆内存,因为堆内存变小,那单次垃圾回收的时间就会降低,但是因为堆内存变小了,就与上面那个目标冲突了,所以这两个目标其实是冲突的。注意:这个目标是单次的垃圾回收时间
  • -XX:ParallelGCThreads=n

    控制吞吐量优先的垃圾回收器在工作时的线程数

接下来我们介绍吞吐量优先的垃圾回收器,那要使用这个吞吐量优先的垃圾回收器我们可以通过-XX:+UseParallelGC ~ -XX:+UseParallelOldGC这样两个开关来把它开启,其实这两个开关在1.8里默认已经是开启的,也就是1.8下我们的jdk默认就使用的是这个 ParallelGC,一个并行的垃圾回收器,那前面这个 UseParallelGC 代表它是一个新生代的垃圾回收器,它采用的算法是复制算法,那么后面这个 UseParallelOldGC呢从它的名字也能猜出来它也是一个并行的垃圾回收器,不过呢它是工作在老年代的,它采用的算法是标记+整理算法,单从它俩的算法上来看跟我们之前介绍的串行的垃圾回收器是一样的,都不会产生内存碎片,区别呢主要就在于它这个 Parallel 这个名词,Parallel 本意就是并行的意思,所以它就暗指了我们这两个垃圾回收器(ParallelGC 和 ParallelOldGC)都是多线程的,当然这两个 Parallel 有点意思,就是你只要开启其中一个,它自动会连带的把另外一个也同时开启,那么它的工作模式是这样的。就是假设有多核cpu,四核的cpu,有四个线程都在跑,这时候呢内存不足了, 触发了一次垃圾回收,那么这时候呢这些用户线程就会跑到一个安全点停下来,这都跟之前的 SerialGC是类似的,停下来以后呢,不一样之处在于垃圾回收器会开启多个线程来进行垃圾回收,这多个回收线程一拥而上,大家一块尽快把垃圾给它清理完,这里这个垃圾回收线程的个数默认情况下是跟你的cpu核数是相关的,只要你的cpu核数是小于一个值的时候,它就跟你的cpu核数是一模一样的,比如说我现在四核cpu,那将来就开四个垃圾回收线程来完成回收工作,当然回收结束以后,再恢复其它线程的运行,那它工作的时候呢,它的一个cpu的使用曲线大概是上图的样子,因为四核cpu大家都去忙垃圾回收了,所以在垃圾回收的过程中cpu的占用率一下子会飚到100%,如果下次又遇到了垃圾回收,又飚到了100%,这是它的垃圾回收发生时对cpu占用率的一个影响。不过这也说得过去,为啥呢,多核cpu我要充分发挥它们的性能,大家一起上,赶紧把这个垃圾回收工作做完了,好恢复剩下用户线程的运行嘛,当然这个线程数我们可以通过一个参数来进行控制,就是 -XX:ParallelGCThreads=n 就可以控制ParallelGC 运行时它的一个线程数了。ParallelGC 它的一个特点是它可以进行一个调整,根据一个目标设置 ParallelGC 的一个工作方式,那与之相关的参数有这么三个比较重要,第一个呢叫做-XX:UseAdaptiveSizePolicy,这个意思呢就是说我采用一个自适应的大小调整策略,调整谁的大小呢,主要就是指调整我们新生代它的一个大小,新生代呢我们以前都说过,分为伊甸园和两个幸存区,那么如果我们这个开关打开,那么在我们的 ParallelGC 工作的时候它就会动态的去调整我们这个伊甸园跟幸存区额一个比例,包括整个堆的大小,它都会进行一个调整,包括那个晋升阈值,也会受这个开关的一个影响,这是一个开关啊,除了这个开关以外,还有俩目标,第一个目标呢 -XX:GCTimeRatio=ratio 和 第二个目标 -XX:MaxGCPauseMillis=ms它俩的含义不一样,目标是相反的,第一个目标叫做 GCTimeRatio,第二个目标叫 MaxGCPauseMillis,它们各自是指什么呢,就是我们的这个 ParallelGC 它比较得智能,它可以根据你的一个设定目标来尝试去调整堆的大小来达到你期望的那个目标,比如说第一个目标 GCTimeRatio,它就是主要是调整我们吞吐量的一个目标,什么意思嘞,我们先来看一个计算公式,它就是用来调整我们这个垃圾回收的时间跟总时间的一个占比,比如说我们先来看这个公式 1/( 1 + ratio) 这么一个公式,那我们来套用这个公式计算一下就明白了,比如说 ratio 的默认值其实是99,那么我们来看,那 1/( 1 + 99) 就是 1/100 等于 0.0.1,那意思是说你垃圾回收的时间不能超过总时间的 1%,换句话说,如果你工作了100分钟,那么这100分钟内,只有1分钟时间能用于垃圾回收,如果达不到这个目标,那么我们的 ParallelGC 回收器它就会尝试去调整堆的大小来达到这个目标,一般呢是把这个堆给它增大,因为增大了以后垃圾回收的次数就变得不频繁了,这样就可以达到我们垃圾回收的总时间下降,从而达到这个吞吐量的一个提高,这是第一个参数。那么第二个参数叫做 MaxGCPauseMillis, Paule 就是暂停的意思,Millis 就是指你这个暂停的毫秒数,前面有个 Max,就是你的最大暂停毫秒数,它的默认值是200毫秒,但是这里大家要注意一点啊,这两个目标其实是冲突的,因为你调整了这个 TimeRatio,意思就是说我的堆要变大,一般情况下就是会把堆调大,堆调大了那么这个吞吐量就提升了,但是堆大了,那你每一次垃圾回收所花费的时间可能就会增长,那就没办法达到这个每一次它这个垃圾回收的暂停时间达到 MaxGCPauseMillis 这个指标了,反之你要让这个暂停时间变得短,那意味着你要让这个堆的空间变小,这样我垃圾回收的时间才会短嘛,但是这样呢你的吞吐量又下来了,所以它俩呢你要取一个折中,就是根据你的实际应用情况取一个合理的值,它俩是一个对立的一个目标,一般呢这个 ratio 它的默认值是99,但是这个99呢很难达到,因为我们刚才也算过了,你就是100分钟内有1分钟的 STW 的垃圾回收暂停时间,这个有点难以达到,所以一般我们这个 ratio 呢设的值一般是19,也就是它是 1/(1+19) = 1/20 = 0.05,也就是100分钟内允许5分钟的垃圾回收时间,这个还是比较容易达到的

4.3 响应时间优先

  • -XX:UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

    开启 ConcMarkSweepGC 垃圾回收器,缩写为 CMS,采用标记+清除算法,是一个响应时间优先的垃圾回收器,它是一个老年代的垃圾回收器,是并发的,因为 CMS 采用标记清除算法,会造成内存碎片的产生,当内存碎片过多会发生并发垃圾,并发失败它会退化为 SerialOld 串行的老年代的垃圾回收器采用标记整理算法执行一次垃圾回收,但因为它是串行的,所以响应时间太长,而 CMS 本来是一个响应时间优先的垃圾回收器,所以这会给用户带来不好的体验。
  • -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads

    -XX:ParallelGCThreads=n ,就是指并行的垃圾回收的线程数,它一般是跟你的cpu核数是一样的,比如cpu核数是4,,这个n是4,但是我们并发的这个 GC 线程数是不一样的,它可以通过另外一个参数 -XX:ConcGCThreads来设置,一般这个参数我们建议设置为并行线程数的1/4,也就是4核cpu,那它的值是1,也就是一个线程占用cpu去进行垃圾回收,剩下三个cpu还要留给用户线程去执行工作,这是跟线程数相关的两个配置。
  • -XX:CMSInitiatingOccupancyFraction=percent

    参数-XX:CMSInitiatingOccupancyFraction=percent呢就是来控制我们何时来进行这个 CMS 垃圾回收的一个时机的, 这个参数名字比较长,它得大概意思就是我们的执行 CMS 垃圾回收的一个内存占比,这个 percent 表示内存占比,比如说它设值为80,那就是当我的内存占用不用到内存不足时,只要老年代的内存占用达到80%的时候,我就执行一次垃圾回收,这样的话是为了预留一些空间给那些浮动垃圾来用。在早期的JVM里面 percent 默认值应该是65%左右,也就是说这个值设的越小,那么 CMS 它进行垃圾回收的触发时机就越早一些。
  • -XX:+CMSScavengeBeforeRemark

    最后一个参数-XX:+CMSScavengeBeforeRemark,就是在我们重新标记的这个阶段有一个特殊的场景,就是有可能新生代的一些对象会引用我们老年代的对象,这时如果在这里进行重新标记时它必须扫描整个堆,然后呢去通过新生代去引用,扫描一遍老年代的对象,做那个可达性的分析,但这样的话其实对我的性能影响有些大,为什么呢,新生代的对象,创建的一些对象个数都比较多,而且其中很多本身是要作为垃圾的,如果我再从新生代来找我们的老年代,这个你就算是找到了将来这些新生代的垃圾也要被回收掉,所以相当于我们在回收之前多做了一些无用的查找工作,那怎么避免这种现象呢,我们就可以用最后这个参数 -XX:+CMSScavengeBeforeRemark 来在你做这个重新标记之前我先对新生代做一次垃圾回收,新生代当然是用UseParNewGC这个新生代的垃圾回收器去做这个回收工作了,做完一次回收以后呢,新生代的存活对象少了那它们将来我扫描的对象就少了,这样呢就能够减轻我重新标记时的一个压力,这就是最后这个 CMSScavengeBeforeRemark 它的一个作用,它只是个开关,你一加就是打开,你一减就是禁用。总结来说就是:在重新标记前做一次垃圾回收清理掉新生代的垃圾,防止无用的查找。

接下来我们介绍响应时间优先的垃圾回收器,叫做 CMS,要开启这个响应时间优先的垃圾回收器,那么它的一个虚拟机参数叫做 -XX:UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld ,Conc是Concurrent并发的意思,Mark是标记的意思,最后那个Sweep就是清除,如果回忆一下我们介绍的垃圾回收算法,从它的名字上大概能够猜出来,它是一款基于标记清除算法的一个垃圾回收器,并且它是并发的,注意Concurrent这个单词跟我们前面介绍的Parallel不一样,Parallel是并行的,Concurrent是并发的,既然提到了这两个名词,我们稍微来解释一下,所谓的Concurrent并发它就是指我们垃圾回收器它在工作的同时其它的用户线程也能同时进行,也就是用户线程跟垃圾回收线程是并发执行,它们都要去抢占cpu,而之前的Parallel并行的含义是指我多个垃圾回收器它们并行运行,但是在此期间在垃圾回收期间它不允许我们的用户工作线程继续运行了,换句话说 STW 了,这一点看起来很好,我们的这款 CMS 这个垃圾回收器它在某些时刻能够起到一个并发的效果,也就是它在工作的同时,用户线程也能工作,这样的话就进一步减少了 STW 的时间,当然它在某几个阶段还是需要 STW 的,但是在垃圾回收的其中一些阶段是不需要 STW 的,它可以跟用户线程并发执行,这也是它最大的一个特点,它是工作在老年代的一款垃圾回收器,与之配合的是一个叫 ParNewGC,它是工作在新生代的一款基于复制算法的垃圾回收器,它俩是一对,一起工作运行,但是 CMS 这个垃圾回收器有的时候它会发生一个并发失败的问题,这个时候它就会采取一个补救的措施让老年代的垃圾回收器从 CMS 这种并发垃圾回收器退化到一个 SerialOld 单线程的垃圾回收器,这个我们在介绍串行垃圾回收器时见过,是一款基于标记整理的工作在老年代的一个垃圾回收器,了解了它的基本参数,那我们下面就来看一下它的工作流程,它的工作流程是相对比较复杂的,首先,多个cpu,它们开始并行执行,现在老年代发生了注意是老年代发生了内存不足,那么这些线程呢都到达了安全点暂停下来了,暂停下来以后呢,这时候 CMS 垃圾回收器开始工作了,它会执行一个初始标记的动作,在这个初始标记动作的时候,仍然需要 STW,也就是我们其它的用户线程就阻塞暂停下来了,但是等到这个初始标记完成了以后呢,初始标记它很快,为什么呢,它只去标记一些根对象,它不会把堆内存所有对象全遍历一遍,它只会列举那些根对象,所以这个初始标记非常的快,暂停时间也是非常短,那等到初始标记结束以后,接下来呢用户线程就可以恢复运行了,与此同时,我们的垃圾回收线程它还可以继续并发标记,把剩余的那些垃圾(因为上次标记只标记了根对象)给它找出来,这里跟用户线程是并发执行的,在这个过程中呢,它不用 STW,所以它的响应时间是很短的,几乎不影响你的用户线程的工作,等到并发标记以后,还要做一步,叫重新标记,这步又要 STW 了,为什么重新标记需要 STW 呢,是因为你在并发标记的同时,用户线程也在工作,它工作的时候就有可能产生一些新的对象,改变一些对象的引用,就可能对你的垃圾回收做了一些干扰,所以呢,等到这个并发标记结束以后,它还要再做一遍重新标记的工作,等重新标记完了,那么用户线程又可以恢复运行,这时候我的垃圾回收线程再做一次并发的清理。我们看到整个工作阶段呢只有在初始标记和重新标记这两个阶段会造成 STW,其它阶段呢都是并发执行的,所以它的响应时间非常的短,也符合我们标题它是一款专门专注于响应时间优先的一个老年代的垃圾回收器,这里呢我们关注几个细节,第一个细节就是它的并发时的线程数,在初始标记时它这个并发线程数呢,受到下面两个参数的影响,第一个还是我们前面前面见过的一个参数 -XX:ParallelGCThreads=n ,就是指并行的垃圾回收的线程数,它一般是跟你的cpu核数是一样的,我们这个例子中应该是4,这个n是4,但是我们并发的这个 GC 线程数是不一样的,它可以通过另外一个参数 -XX:ConcGCThreads来设置,一般这个参数我们建议设置为并行线程数的1/4,也就是4核cpu,那它的值是1,也就是一个线程占用cpu去进行垃圾回收,剩下三个cpu还要留给用户线程去执行工作,好,这是跟线程数相关的两个配置。讲到这呢,我们稍微说一下它这个 CMS 这个垃圾回收工作时它的一个对cpu的占用那它对cpu的占用实际上不如之前的 ParallelGC 对cpu的占用高,因为就拿刚才的例子来讲,它四核的cpu它只占了一核做垃圾回收,所以它对cpu的占用并不高,但是我们的用户工作线程是不是也在运行啊,但用户工作线程本来它要运行时它是可以满核的四核cpu都能使用上,但是其中1核呢被垃圾回收的线程占用了,所以用户工作线程只能占用原来的3/4的cpu的数量了,所以对我们整个应用程序的吞吐量实际上是有影响的,本来你四核去完成逻辑计算,可能只需要花费1s钟的时间但是呢其中一核给了垃圾回收用了,那我可用于计算的cpu个数就少了,那你计算同样的工作量它的花费的时间可能就会变长了,所以我们这个 CMS它虽然做到了响应时间优先,但是由于它占用了一定cpu的使用量,因此呢它对我们整个应用程序的这个吞吐量是有一些影响的。接下来我们把其它的几个参数也分别介绍一下,在我们的这个 CMS 这个垃圾回收器工作的过程中呢,它在执行并发清理的时候由于其它的用户线程还可以继续运行其它的用户线程在运行的同时,可能又会产生新的垃圾,所以并发清理的同时它不能把这些新的垃圾干掉,所以它就得等到下次垃圾回收时再清理这些在垃圾清理的同时又产生的这些新垃圾,这些新垃圾呢我们把它称之为叫浮动垃圾,这些浮动垃圾得等到下次做垃圾回收时才能清理掉,但是这样就带来一个问题,因为你在垃圾回收的过程中可能产生新的垃圾,那它就不能像原来其它的垃圾回收器那样等到整个堆内存不足了我再做垃圾回收,那样的话那这些新垃圾呢就没处放了,所以你得预留一些空间来保留这些浮动垃圾,那这个参数-XX:CMSInitiatingOccupancyFraction=percent呢就是来控制我们何时来进行这个 CMS 垃圾回收的一个时机的, 这个参数名字比较长,它得大概意思就是我们的执行 CMS 垃圾回收的一个内存占比,这个 percent 表示内存占比,比如说它设值为80,那就是当我的内存占用不用到内存不足时,只要老年代的内存占用达到80%的时候,我就执行一次垃圾回收,这样的话是为了预留一些空间给那些浮动垃圾来用。在早期的JVM里面 percent 默认值应该是65%左右,也就是说这个值设的越小,那么 CMS 它进行垃圾回收的触发时机就越早一些。最后一个参数-XX:+CMSScavengeBeforeRemark,就是在我们重新标记的这个阶段有一个特殊的场景,就是有可能新生代的一些对象会引用我们老年代的对象,这时如果在这里进行重新标记时它必须扫描整个堆,然后呢去通过新生代去引用,扫描一遍老年代的对象,做那个可达性的分析,但这样的话其实对我的性能影响有些大,为什么呢,新生代的对象,创建的一些对象个数都比较多,而且其中很多本身是要作为垃圾的,如果我再从新生代来找我们的老年代,这个你就算是找到了将来这些新生代的垃圾也要被回收掉,所以相当于我们在回收之前多做了一些无用的查找工作,那怎么避免这种现象呢,我们就可以用最后这个参数 -XX:+CMSScavengeBeforeRemark 来在你做这个重新标记之前我先对新生代做一次垃圾回收,新生代当然是用UseParNewGC这个新生代的垃圾回收器去做这个回收工作了,做完一次回收以后呢,新生代的存活对象少了那它们将来我扫描的对象就少了,这样呢就能够减轻我重新标记时的一个压力,这就是最后这个 CMSScavengeBeforeRemark 它的一个作用,它只是个开关,你一加就是打开,你一减就是禁用。还有一点直接在讲CMS的时候我们说过它这个CMS呢,它有一个特点,就是在内存碎片比较多的情况下,因为它是基于一种标记+清除算法,它有可能产生比较多的内存碎片,这样的话呢就会造成将来我分配对象时 Minor GC 不足,结果老年代由于碎片过多也不足,这样呢就会造成一个并发失败,这时候相当于碎片太多造成并发失败,造成并发失败呢那我这个叫 CMS 的老年代的垃圾回收器就不能正常工作了,这个时候那我们这个垃圾回收器就会退化为SerialOld 做一次单线程的串行的一个垃圾回收,做一些整理,整理完了那个碎片减少了我才能继续恢复工作,但一旦发生了这种并发失败问题,这个垃圾回收的时间就会一下子飙上来,这也是这个 CMS 垃圾回收器的一个最大问题,就是由于内存碎片将来可能过多,会导致它并发失败,并发失败了就退化为 SerialOld 了,那这样呢垃圾回收的时间一下子就会变得很长,导致本来我是一个响应时间优先的垃圾回收器,结果你响应时间一下变得很长,这样就给用户造成了一个不好的体验,所以这也是 CMS 这种垃圾回收器它存在的一个最大的问题,
文章一大堆文字堆在一起,外观上看起来很不好看,但是在文字里详细记述了原理,是连在一起的,所以我就没有拆开,我认为耐心读下去并且边读边理解将会有很大的收获。