10. 系统分析垃圾收集器

一、垃圾收集算法

垃圾收集常用的算法有三种。标记-清除算法,标记-复制算法,标记-整理算法。下面一个一个来看:

1.1标记清除算法

标记清除算法分为“标记”和“清除”两个阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标 记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。

1.1.1 标记清除算法的原理

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记: Collector从引用根结点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

  • 清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

1.1.2 标记清除算法存在的问题

标记清除算法是最基础的收集算法,比较简单,但是会带来 两个明显的问题:

1. 效率问题

  • 如果需要标记的对象太多,效率不高
  • 如果内存空间太大,效率也不高

2. 空间问题

  • 标记清除后会产生大量不连续的碎片

1.2标记复制算法

标记复制算法包含两个步骤:标记和复制。

1.2.1 标记复制算法的原理

标记复制算法的原理是,将指定的一块内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。不难想象,在下一次GC之后,左边将会再次变成活动区间。如下图:

1.2.2 标记复制算法存在的问题

标记复制算法需要两块空间,对内存要求比较大,内存的利用率比较低。适用于短生存期的对象,持续复制长生存期的对象则导致效率降低

1.3 标记整理算法

1.3.1 标记-整理算法的原理

标记整理算法的标记过程和标记-清除算法一样,因为标记清除算法会导致很多留下来的内存空间碎片,随着碎片的增多,严重影响内存读写的性能,所以在标记-清除之后,会对内存的碎片进行整理。让所有存活的对象向一端移动,然后直接清理掉另一端的内存。由于压缩空间需要一定的时间,会影响垃圾收集的时间。通常用在老年代,这也是老年代耗时多的原因之一。如下图:

1.3.2 标记整理算法存在的问题

标记整理是标记清除的扩展版,在标记清除以后,对内存空间进行整理。这样会更耗费时间。

二、分代收集理论

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

通常,我们将java堆分为新生代和老年代,“分代收集”(Generational Collection)就是根据堆划分的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,所以可以选择复制算法,只需要付出少量存活对象的复制成本就可以完成每次垃圾收集。而老年代中对象存活的概率是比较高的,而且没有额外空间对它进行分配担保,就使用“标记-清除”或“标记-整理”算法来进行回收。“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

三、垃圾收集器

垃圾收集器按照堆空间分类方法分为新生代垃圾收集器,老年代垃圾收集器。常见的新生代垃圾收集器有:Serial、ParNew、Parallel;常见的老年代垃圾收集器有:CMS、Serial Old、Parallel Old。还有既有新生代又有老年代的收集器,如:G1、ZGC等。不同类型的垃圾收集器采用的垃圾收集算法是不同的。通常新生代使用的是标记-复制算法;老年代使用的是标记清除和标记整理算法。

常见的垃圾收集器如下图:

Serial、ParNew、Parallel Scavenge用于新生代;CMS、Serial Old、Paralled Old用于老年代。并且他们之间以相对固定的组合使用(具体组合关系如上图)。G1是一个独立的收集器不依赖其他6种收集器。ZGC是目前JDK 11的实验收集器。下面来研究一下各种类型的垃圾收集器

3.1 Serial收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。Serial收集器是一个单线程收集器。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

因为新生代的特点是对象存活率低,所以收集算法用的是【标记复制】算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好。 如下图:

Serial收集器是新生代垃圾收集器,其对应的Serial Old是老年代垃圾收集器。Serial Old也是单线程收集器,它主要有两大用途:

  • 一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,
  • 另一种用途是作为CMS收集器的后备方案。

Serial收集器参数配置

启用Serial收集器, 启用Serial Old收集器
-XX:+UseSerialGC 
-XX:+UseSerialOldGC

3.2 Parallel收集器

Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

Parallel Scavenge收集器是一个新生代收集器,采用标记复制算法,并行手机垃圾。该收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

什么是吞吐量呢?

就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 即:

Parallel Scavenge收集器提供两个参数控制垃圾回收的执行:

  • -XX:MaxGCPauseMillis,最大垃圾回收停顿时间。这个参数的原理是空间换时间,收集器会控制新生代的区域大小,从而尽可能保证回收少于这个最大停顿时间。简单的说就是回收的区域越小,那么耗费的时间也越小。
    所以这个参数并不是设置得越小越好。设太小的话,新生代空间会太小,从而更频繁的触发GC。
  • -XX:GCTimeRatio,垃圾回收时间与总时间占比。这个是吞吐量的倒数,原理和MaxGCPauseMillis相同。

因为Parallel Scavenge收集器关注的是吞吐量,所以当设置好以上参数的时候,同时不想设置各个区域大小(新生代,老年代等)。可以开启-XX:UseAdaptiveSizePolicy参数,让JVM监控收集的性能,动态调整这些区域大小参数。

新生代采用复制算法,老年代采用标记-整理算法。

Parallel垃圾收集器对应的老年代垃圾收集器是Parallel Old。Parallel Old采用的也是多线程收集垃圾。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

JDK8默认的新生代和老年代收集器

3.3 ParNew收集器

ParNew同样用于新生代,跟Parallel收集器很类似,也是采用多线程的方式收集垃圾,Par是Parallel的缩写。ParNew收集器工作的时候同样需要STW(Stop The World)。ParNew主要和CMS收集器配合使用。另外Parallel收集器更多关注的是吞吐量。当对吞吐量以及CPU要求比较高的情况下,建议使用Parallel收集器。

因为是多线程执行,所以在多CPU下,ParNew效果通常会比Serial好。但如果是单CPU则会因为线程的切换,性能反而更差。

ParNew收集器是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

参数设置

使用-XX:+UseConcMarkSweepGC选项后默认新生代收集器为ParNew收集器;
使用-XX:+UseParNewGC选项强制指定使用ParNew收集器;
使用-XX:ParallelGCThreads参数限制垃圾收集的线程数;

3.4 CMS收集器

1.什么是CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。是基于多线程的“标记-清除”算法

CMS非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

2.CMS收集器的工作原理

CMS整个过程比之前的收集器要复杂,整个过程分为四步:

第一步:初始标记。(Stop The World) 只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

第二步:并发标记。(Stop The World) 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

第三步:重新标记 。为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

第四步:并发清除 。这里包含两个步骤。并发清理和线程重置。

  • 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。 ,和用户线程一起工作,不需要暂停工作线程。
  • 线程重置:重置本次GC过程中的标记数据。

由于耗时最长的并发标记并发清除过程中,垃圾收集线程可以和用户一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

是一款优秀的垃圾收集器,具有并发收集、低停顿的优点。但也有几个非常明显的缺点:

  • 对CPU资源敏感(会和服务抢资源);
  • 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。我们可以通过参数设置让jvm在执行完标记清除以后进行整理
XX:+UseCMSCompactAtFullCollection    //可以让jvm在执行完标记清除后再做整理
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrentmode failure",此时会进入stop the world,用serial old垃圾收集器来回收

3. cms相关的参数

1. -XX:+UseConcMarkSweepGC:启用cms 
2. -XX:ConcGCThreads:并发的GC线程数 
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片) 
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比) 
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整 
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段 
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW 
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

4. 既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?

答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World”这种场景下使用。



posted @ 2021-10-21 11:20  盛开的太阳  阅读(170)  评论(0编辑  收藏  举报