一文了解JVM所有垃圾回收器,从Serial到ZGC

《对象搜索算法与回收算法》介绍了垃圾回收的基础算法,至关于垃圾回收的方法论。接下来就详细看看垃圾回收的具体实现。算法

上文提到过现代的商用虚拟机的都是采用分代收集的,不一样的区域用不一样的收集器。经常使用的7种收集器,其适用的范围如图所示微信


Serial、ParNew、Parallel Scavenge用于新生代;
CMS、Serial Old、Paralled Old用于老年代。
而且他们相互之间以相对固定的组合使用(具体组合关系如上图)。G1是一个独立的收集器不依赖其余6种收集器。ZGC是目前JDK 11的实验收集器。多线程

下面来看看各个收集器的特性并发

Serial收集器

Serial,是单线程执行垃圾回收的。当须要执行垃圾回收时,程序会暂停一切手上的工做,而后单线程执行垃圾回收。app

由于新生代的特色是对象存活率低,因此收集算法用的是复制算法,把新生代存活对象复制到老年代,复制的内容很少,性能较好。

单线程地好处就是减小上下文切换,减小系统资源的开销。但这种方式的缺点也很明显,在GC的过程当中,会暂停程序的执行。若GC不是频繁发生,这或许是一个不错的选择,不然将会影响程序的执行性能。
对于新生代来讲,区域比较小,停顿时间短,因此比较使用。jvm

ParNew收集器

ParNew一样用于新生代,是Serial的多线程版本,而且在参数、算法(一样是复制算法)上也彻底和Serial相同。高并发

Par是Parallel的缩写,但它的并行仅仅指的是收集多线程并行,并非收集和原程序能够并行进行。ParNew也是须要暂停程序一切的工做,而后多线程执行垃圾回收。

由于是多线程执行,因此在多CPU下,ParNew效果一般会比Serial好。但若是是单CPU则会由于线程的切换,性能反而更差。性能

Parallel Scavenge收集器

新生代的收集器,一样用的是复制算法,也是并行多线程收集。与ParNew最大的不一样,它关注的是垃圾回收的吞吐量。线程

这里的吞吐量指的是 总时间与垃圾回收时间的比例。这个比例越高,证实垃圾回收占整个程序运行的比例越小。指针

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

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

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

Serial Old收集器

老年代的收集器,与Serial同样是单线程,不一样的是算法用的是标记-整理(Mark-Compact)。

由于老年代里面对象的存活率高,若是依旧是用复制算法,须要复制的内容较多,性能较差。而且在极端状况下,当存活为100%时,没有办法用复制算法。因此须要用Mark-Compact,以有效地避免这些问题。

Parallel Old收集器

老年代的收集器,是Parallel Scavenge老年代的版本。其中的算法替换成Mark-Compact。

CMS收集器

CMS,Concurrent Mark Sweep,一样是老年代的收集器。它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。

命名中用的是concurrent,而不是parallel,说明这个收集器是有与工做执行并发的能力的。MS则说明算法用的是Mark Sweep算法。

来看看具体地工做原理。CMS整个过程比以前的收集器要复杂,整个过程分为四步:

  • 初始标记(initial mark),单线程执行,须要“Stop The World”,但仅仅把GC Roots的直接关联可达的对象给标记一下,因为直接关联对象比较小,因此这里的速度很是快。
  • 并发标记(concurrent mark),对于初始标记过程所标记的初始标记对象,进行并发追踪标记,此时其余线程仍能够继续工做。此处时间较长,但不停顿。
  • 从新标记(remark),在并发标记的过程当中,因为可能还会产生新的垃圾,因此此时须要从新标记新产生的垃圾。此处执行并行标记,与用户线程不并发,因此依然是“Stop The World”,时间比初始时间要长一点。
  • 并发清除(concurrent sweep),并发清除以前所标记的垃圾。其余用户线程仍能够工做,不须要停顿。


因为最耗费时间的并发标记与并发清除阶段都不须要暂停工做,因此总体的回收是低停顿的。

因为CMS以上特性,缺点也是比较明显的,

  • Mark Sweep算法会致使内存碎片比较多
  • CMS的并发能力依赖于CPU资源,因此在CPU数少和CPU资源紧张的状况下,性能较差
  • 并发清除阶段,用户线程依然在运行,因此依然会产生新的垃圾,此阶段的垃圾并不会再本次GC中回收,而放到下次。因此GC不能等待内存耗尽的时候才进行GC,这样的话会致使并发清除的时候,用户线程能够了利用的空间不足。因此这里会浪费一些内存空间给用户线程预留。

有人会以为既然Mark Sweep会形成内存碎片,那么为何不把算法换成Mark Compact呢?

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

G1收集器

G1,Garbage First,在JDK 1.7版本正式启用,是当时最前沿的垃圾收集器。G1能够说是CMS的终极改进版,解决了CMS内存碎片、更多的内存空间登问题。虽然流程与CMS比较类似,但底层的原理已经是彻底不一样。

高效益优先。G1会预测垃圾回收的停顿时间,原理是计算老年代对象的效益率,优先回收最大效益的对象。

堆内存结构的不一样。之前的收集器分代是划分新生代、老年代、持久代等。

G1则是把内存分为多个大小相同的区域Region,每一个Region拥有各自的分代属性,但这些分代不须要连续。

这样的分区能够有效避免内存碎片化问题。

可是这样一样会引伸一个新的问题,就是分代的内存不连续,致使在GC搜索垃圾对象的时候须要全盘扫描找出引用内存所在。

为了解决这个问题,G1对于每一个Region都维护一个Remembered Set,用于记录对象引用的状况。当GC发生的时候根据Remembered Set的引用状况去搜索。

两种GC模式

  • Young GC,关注于全部年轻代的Region,经过控制收集年轻代的Region个数,从而控制GC的回收时间。
  • Mixed GC,关注于全部年轻代的Region,而且加上经过预测计算最大收益的若干个老年代Region。

总体的执行流程:

  • 初始标记(initial mark),标记了从GC Root开始直接关联可达的对象。STW(Stop the World)执行。
  • 并发标记(concurrent marking),并发标记初始标记的对象,此时用户线程依然能够执行。
  • 最终标记(Remark),STW,标记再并发标记过程当中产生的垃圾。
  • 筛选回收(Live Data Counting And Evacuation),评估标记垃圾,根据GC模式回收垃圾。STW执行。


在Region层面上,总体的算法偏向于Mark-Compact。由于是Compact,会影响用户线程执行,因此回收阶段须要STW执行。

使人惊叹的ZGC

在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。

ZGC几乎在全部地方并发执行的,除了初始标记的是STW的。因此停顿时间几乎就耗费在初始标记上,这部分的实际是很是少的。那么其余阶段是怎么作到能够并发执行的呢?

ZGC主要新增了两项技术,一个是着色指针Colored Pointer,另外一个是读屏障Load Barrier

着色指针Colored Pointer
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked一、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。至关于在对象的指针上标注了对象的信息。注意,这里的指针至关于Java术语当中的引用。

在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。

在G1的时候就说到过,Compact阶段是须要STW,不然会影响用户线程执行。那么怎么解决这个问题呢?

读屏障Load Barrier
因为着色指针的存在,在程序运行时访问对象的时候,能够轻易知道对象在内存的存储状态(经过指针访问对象),若请求读的内存在被着色了。那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有必定的耗费,从而达到与用户线程并发的效果。

把这两项技术联合下理解,引用R大(RednaxelaFX)的话

与标记对象的传统算法相比,ZGC在指针上作标记,在访问指针时加入Load Barrier(读屏障),好比当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有几率被减速,而不存在为了保持应用与GC一致而粗暴总体的Stop The World。

ZGC虽然目前还在JDK 11还在实验阶段,但因为算法与思想是一个很是大的提高,相信在将来不久会成为主流的GC收集器使用。


更多技术文章、精彩干货,请关注
博客:zackku.com
微信公众号:Zack说码

相关文章
相关标签/搜索