一、垃圾收集算法
垃圾收集常用的算法有三种:标记-清理算法,标记-复制算法,标记整理算法。下面一个一个来看:
1.1 标记-清除算法
标记清除算法分为“标记”和“清除”两个阶段:标记存活的对象,统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
1.1.1 标记-清除算法的原理
当堆中的有效内存空间被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:Collector从引用跟结点开始遍历,标记所有被引用的对象。一般是在对象的header中记录为可达对象。
- 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其header中没有标记为可达对象,则将其回收。
1.1.2 标记清除算法存在的问题
标记清除算法是最基础的收集算法,比较简单,但是会带来两个明显的问题:
1、效率问题
- 如果需要标记的对象太多,效率不高
- 如果内存空间太大,效率不高
2、空间问题
- 标记清除后会产生大量不连续的碎片
1.2 标记-复制算法
标记复制算法包含两个步骤:标记和复制。
1.2.1 标记复制算法的原理
标记复制算法的原理是,将指定的一块内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。不难想象,在下一次GC之后,左边将会再次变成活动区间,如下图:
1.2.1 标记复制算法存在的问题
标记复制算法需要两块空间,对内存要求比较大,内存的利用率比较低。使用于短期生存的对象,持续复制长期生存的对象则导致效率低
1.3 标记-整理算法
1.3.1 标记-整理算法的原理
标记整理算法的标记过程与标记-清除算法一样,因为标记清除算法会导致很多留下来的内存空间碎片化,随着碎片的增多,严重影响内存读写的性能,所在标记-清除后,会对内存的碎片进行整理。让所有存活的对象向一端移动,然后直接清理掉另一端的内存。由于压缩空间需要一定的时间,会影响垃圾收集时间。通常用在老年代,这也是老年代耗时多的原因之一。如下图:
1.3.2 标记整理算法存在的问题
标记整理是标记清除后的扩展,在标记清除以后,对内存空间进行整理。这样会更耗费时间。
三、垃圾收集器
垃圾收集器按照堆空间分类方法分为新生代垃圾收集器和老年代垃圾收集器。常见的新生代垃圾收集器有:Serial、ParNew、Parallel;常见的老年代收集器有:CMS、Serial Old、Parallel Old。还有既有新生代又有老年代的收集器,如:G1、ZG等。不同类型的垃圾收集器采用的垃圾收集算法是不同的。通常新生代使用的是标记-复制算法;老年代使用的是标记清除和标记整理算法。
常见的垃圾收集器如下图:
Serial、ParNew、Parallel 收集器用于新生代,CMS、Serial Old、Parallel Old 用于老年代。
并且他们之间以相对固定的组合用(具体组合关系如上图)。G1是一个独立的收集器不依赖其他6中收集器。ZGC是目前JDK11的实验收集器。下面来研究一下各种类型的垃圾收集器
3.1 Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。Serial收集器是一个单线程收集器。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(“Stop the world”),知道它收集结束。
因为新生代的特点是对象存活率低,所以收集算法用的是【标记复制】算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好。如下图:
Serial收集器是新生代垃圾收集器,期对应的Serial Old是老年代垃圾收集器。Serical 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:MaxGCPauserMillis,最大垃圾回收停顿时间,这个参数的原理是空间换时间,收集器会控制新生代的区域大小,从而尽可能保证回收少于这个最大停顿时间。简单的说就是回收的区域越小,那么耗费的时间也越小。
- -XX:GCTimeRatio,垃圾回收时间与总时间占比。这个是吞吐量的倒数,原理和MaxGCPauseMillis相同。
因为Parallel Scavenge收集器关注的是吞吐量,所以当设置好以上参数的时候,同时不想设置各个区域大小(新生代,老年代等)。可以开启-XX:UseAdaptiveSizePolicy参数,让JVM监控收集的性能,动态调整这些区域大小参数。
Parallel 垃圾收集器对应的老年代垃圾收集器是Parallel Old。Parallel Old采用的也是多线程收集垃圾。在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器。(JDK1.8默认的新生代和老年代的收集器)
参数设置
-XX:+UseParallelGC设置新生代垃圾收集器为Parallel
-XX:+UseParallelOldGC设置老年代垃圾收集器为Parallel Old
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收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
新生代采用复制算法,老年代采用标记-整理算法。
如下图:ParNew收集器也可以和Serial Old配合工作(主要用途还是与CMS配合)
参数设置
使用-XX:+UseConcMarkSweepGC选项后默认新生代收集器为ParNew收集器;
使用-XX:+UserParNewGC选项强制指定使用ParNew收集器;
使用--XX:ParallelGCThreads参数限制垃圾收集的线程数;
3.4 CMS收集器
1、什么是CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它在垃圾收集时使得用户线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。是基于多线程的“标记-清除”算法。
CMS非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
2、CMS收集器的工作原理
CMS整个过程比之前的收集器要复杂,整个过程分为四步:
第一步:初始标记。(Stop The World)只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
第二步:并发标记。并发标记阶段就是从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更适合“STW”的场景使用。