开往虚拟机的车已经出发,关注上车
虚拟机的垃圾回收器,没有哪一个是绝对好的,只有比较好的。
今天的这篇文章,我要与你分享虚拟机的那些垃圾回收器们。内容不多,可以耐心看完。
垃圾收集器
之前我们已经了解过具体的 GC 涉及的细节知识点,现在我们站在更高的角度,来看看各种垃圾收集器,以及其中两个的工作过程(CMS 和 G1)。
我们知道,虚拟机将保存对象实例的区域分为了两个叫做 新生代 和 老年代 的地方,为此虚拟机针对不同的内存区域利用不同的算法设计了不同的垃圾收集器。
了解垃圾收集器之前,我觉得还是有必要在复习一下 Stop The World ,其用来形容在安全点用户线程暂停的这种状态的一个叫法。关于这个垃圾收集器工作的时候为什么要 Stop The World 还有一个比较有意思的事 ,“你妈妈在给你打扫房间的时候, 肯定也会让你老老实实地在椅子上或者房间外待着, 如果她一边打扫, 你一边乱扔纸屑, 这房间还能打扫完? ”这是虚拟机团队对 Stop The World 的说明,嗯,很有道理,哈哈哈。
那些回收 JVM 垃圾的家伙
Serial
关键字:新生代、Stop The World 、标记 - 复制算法、单线程
Serial 是作用在新生代的垃圾收集器,单线程工作,在工作的时候需要 Stop The World,(包括之前提到的 GC 工作,都是指可达性分析)。采用的是标记 - 复制算法,(关于标记 - 复制的内容之前有讲过就不再展开说了)。
看起来 Serial 收集器没什么特别的,但实际上 Serial 收集器在一些特殊的场景下有着不错的表现,这些要得益于他的额外内存消耗,因为其相比其他收集器要小一些,所以在服务器资源受限的情况下(单核或较少核心以及内存紧张),这个简单的单线程收集器效率还是很可观的。
ParNew
关键字:新生代、Stop The World 、标记 - 复制算法、多线程、绝配
ParNew 可以看成 new parallel gc ,一个新的并行垃圾收集器。是一个对标 Serial 的收集器,它与 Serial 的区别就是它在工作的时候是使用多线程进行工作的。还有,它是目前(JDK 9 以后)唯一一个能配合 CMS 在新生代工作的垃圾收集器。
Parallel Scavenge
关键字:新生代、Stop The World、标记 - 复制算法、多线程、可控吞吐量【用户线程执行时间 /(用户线程执行时间+GC线程执行时间)】、自适应策略
与 ParNew 相比,Parallel Scavenge 多了一些额外的功能,停顿时间和吞吐量可控(通过参数配置-XX: MaxGCPauseMillis 单位毫秒,控制每次垃圾回收的最大停顿时间。-XX: GCTimeRatio 控制吞吐量 0 - 100 整数,1/(1+设置的参数) = 垃圾收集时间占程序执行的总时间比率。
对于 Parallel Scavenge 还有一个特点,就是自适应策略,把内存管理工作完全交由虚拟机,通过参数 -XX: +UseAdaptiveSizePolicy 启用自适应内存策略,这样就不需要指定内存参数,比如新生代大小、eden survivor 比例 晋升年龄等参数。虚拟机会根据当前系统运行状态动态的调整,达到一个合适的停顿时间和吞吐量。
上面这三种 Serial 、 ParNew 、 Parallel Scavenge 都是新生代的垃圾收集器,下面我们来看看老年代的垃圾收集器。
Serial Old
关键字:老年代、Stop The World、标记 - 整理算法、单线程、替补
Serial Old 是和 Serial 一样的一款收集器,只不过,它是工作在老年代的。换句话说就是 Serial Old 就是 Serial 的老年代版本。Serial Old 有一个特殊的用途就是作为 CMS 并发清除失败的时候的替补,这里后面 CMS 收集器再看。
Parallel Old
关键字:老年代、Stop The World、标记 - 整理算法、多线程、CP(组合)
Parallel Old 是一个多线程并行的老年代垃圾收集器。它的出现也是为了解决吞吐量最大化的问题。因为他没出现之前,只有 Serial Old 一款老年代垃圾收集器来配合 Parallel Scavenge ,因为 Serial Old 的单线程性能原因,导致 Parallel Scavenge 吞吐量的优势体现不出来。直到它的出现,它与 Paralle Scavenge 就组成了一对完美的吞吐量 CP。
CMS
关键字:老年代、Stop The World、标记 - 清除算法、多线程、短停顿
Concurrent Mark Sweep 的设计初衷就是要停顿的时间最短!JDK 5 开始使用,JDK 9 以前最优秀,为啥是 JDK 9 以前最优秀,因为 JDK 9 发布的时候,默认启用了 G1 收集器,同时你如果手动改成 CMS 的话,会受到一个 CMS 被声明为不推荐的警告,下面是它具体的工作过程,一共经历 4 个阶段
具体步骤如下:
- 初始标记:需要 Stop The World
- 标记 GC Roots 直接关联的对象(直达),速度较快,停顿时间短。
- 并发标记:与用户线程并发
- 进行标记追踪,完成全部对象的标记任务。可能出现漏标或错标情况。
- 重新标记:需要 Stop The World
- 修正并发标记阶段的对象标记,因为大部分对象不需要修正,所以执行时间相比并发标记时间短,但是停顿时间要比初始标记停顿时间长。
- 并发清除:与用户线程并发
- 耗时较长,可以与用户线程共同工作。
CMS 虽然有了一个较短的停顿时间,但是也有一些其他随之而来的问题。
优点
- 并发执行速度快、停顿时间短。这一点没得说,因为它其中有两个阶段是和用户线程并发。
缺点
- 占用线程资源,因为 CMS 工作有两个阶段是和用户线程并发,所以这里便会抢占用户线程资源。
- 浮动垃圾,清理一次之后还会有清理不掉的对象,需要在下次清理的时候才能够清理到。这里的原因是因为并发清除阶段是和用户线程并发,一边清除一边使用,可能会出现一些无法清理掉的新生垃圾,比如清理过程中,程序断开了某个引用,被断开的引用 GC Roots 不可达,所以这个被断开的引用指向的对象变成了浮动垃圾。
- 空间利用率低,因为并发清理的原因,所以不能等到内存完全用完之后再做清理,所以需要当内存使用达到一定阈值(默认值68%,JDK6的时候提高到了 92%)时就开始进行垃圾回收动作,具体数值可以通过参数控制。这里 JVM 给了风险预案:冻结用户线程,启动 serial old 来进行一次老年代垃圾回收。这也是上面我们说 Serial Old 的时候,提到他的关键字里有 “替补” 的原因。
- 空间碎片,因为使用标记-清除算法的原因,会导致碎片空间的产生。CMS 的做法是在其不能够满足对象分配任务的时候,FULL GC 的时候,会进行一次空间整理的动作。对这个整理的动作也是有参数可以进行控制,参数设置情况为,满足几次FullGC之后,进行一次空间整理,默认值为 0 ,即每次 full gc 都会进行一次空间整理。这一点虽然是缺点,不过 CMS 已经尽力去你补了,包括这里的 FULL GC 之后的内存空间整理,还有对象分配时 CMS 会在 Free List 申请一块较大的内存空间,然后通过指针碰撞的方式来进行对象分配,尽可能减小空间碎片的产生。空间碎片问题也是 CMS 不能直接使用指针碰撞的方式来为对象分配内存的原因。
Garbage First (G1)
关键字:里程碑、JDK 9、区域管理、按需回收、延迟可控的最高吞吐量
要说 CMS 是一个划时代收集器,那 G1 可以称得上划时代的划时代收集器,其作为 JVM 的垃圾收集器的里程碑是有一定原因的,我们继续往下看。
G1 出现的原因也很简单,那就是替换掉 CMS 。G1的设计是颠覆性的设计思路,它跳出了内存一定要划分新生代老年代的这个枷锁,它的工作模式为 Mixed Mode。并且 JDK 9 的时候开始启用,成为了服务端模式下的默认垃圾收集器,替换掉了原来的吞吐量组合(Parallel Scavenge + Parallel Old),同时 CMS 被声明为不推荐使用,CMS 也是从JDK 9 开始准备退役。
按需回收说的是 G1 在做清理的时候,是依据一个可预测停顿时间模型来做的,这是个什么东西呢?简单来说就是,在清理之前,G1 对每个待回收的区域根据回收价值和时间进行排序,然后根据用户所期望的停顿时间来做一个最优回收,后面会继续说。
多了解一点,关于 G1 的工作模式,Mixed Mode 的扩展:G1 有纯 GC 模式和分代回收模式,分代模式会分为 Minor GC 和 Mixed GC 两种,这里的模式选择会影响最后的筛选回收阶段的回收集合的内容。这块内容可参考后面留的 R大 的链接
上面有一点展开说一下,就是 G1 不是没有分代这种操作了吗?是通过内存区域来管理垃圾的,但是事实上 G1 将内存分成多个大小相等的 Region(区域) ,这些 Region 都可以作为Eden、Survivor、Humongous(Humongous 同老年代的作用)。
下面我们一起了解一下 G1 工作的具体步骤:
- 初始标记:Stop The World
- 标记 GC Roots 直接关联的对象,同时修改 TAMS 指针
- 并发标记:
- 标记全部要回收的对象,与用户线程并发,标记完成之后,重新处理 SATB 记录下在并发时有引用变动的对象
- 最终标记:Stop The World
- 处理并发标记阶段 SATB 遗留的引用,同时这个阶段也进行弱引用处理。
- 筛选回收:Stop The World
- 这个阶段会更新 Region 的统计数据,对每个 Region 根据其回收价值和成本进行排序,然后根据用户所期望的停顿时间(参数设置)来制定一个回收计划。
- 再根据这个计划,选择任意 Region 来组成一个回收集(collection set)。将回收集中的 Region(被选中的区域) 中的存活对象复制到空的 Region 中,然后将旧的(选中的) Region 清理掉。
- 以上过程由多线程并行完成,同时因为移动对象需要暂停用户线程(Stop The World)
TAMS:Top at Mark Start Region 中的两个指针名称,他们的作用是将 Region 的一部分空间划分出来给并发回收过程中程序运行产生的新对象使用 SATB:原始快照,还记得之前我们对漏标的解决方案吗?一种是增量更新(CMS 采用的这种方案),另外一个就是原始快照,这里可以翻翻之前的内容。
G1除了并发标记阶段都需要暂停用户线程
G1的理想目标:在延迟可控的情况下达到最大的吞吐量
用户可以通过参数设置所期望的停顿时间,这个时间一般建议设置为 100 ~ 300 ms。
垃圾收集器小结
上面一共说了 7 款垃圾收集器,不过他们的具体使用我觉得有必要了解一下。
按照年代划分
新生代
- Serial
- ParNew
- Parallel Scavenge
老年代
- Serial Old
- Parallel Old
- CMS
内存区域:G1
搭配组合
因为不同的阶段,垃圾收集器之间的搭配不同,所以我们就按照 JDK 9 作为划分界线,来看下 JDK 9 前后的搭配情况
通过搭配关系我们可以看出,JDK 9 以前,Hotspot 提供了多种选择,而且场面看起来很和谐,解释一下 CMS 与 Serial Old 之间的虚线,这代表 CMS 并发清除失败的时候,以 Serial Old 作为备选方案的组合。
JDK 9 之后,因为 G1 的出现,hotspot 取消了两种方案的支持(Serial + CMS 和 ParNew + CMS),仅提供了 4 种虚拟机搭配方案,他们分别是
- Serial + Serial Old 的单线程组合,适用于资源受限的场景。
- ParNew + CMS 这组曾经的王者组合,新生代的多线程并行高性能加上老年代的短暂停顿组合,可以应对大部分场景。
- Parallel Scavenge + Serial Old 这是 Paralle Old 没出现的时候的应对组合(不明白为何 JDK 9 的时候没取消这对奇葩组合)
- Parallel Scavenge + Parallel Old 这对高吞吐量 CP
看起来这 4 组搭配很完美,不过因为 G1 的出现,看起来的美好也没那么好了 G1在 server 模式下取代了高吞吐量的 CP (Parallel Scavenge + Parallel Old)成为了默认的垃圾收集器。
同时在 JDK 9 使用 ParNew + CMS 这组搭配时,还会收到来自 hotspot 的警告,CMS 已经被声明为不推荐使用,因为 ParNew 此时只能与 CMS 搭配使用,所以可以说当时 CMS 拯救了 ParNew 的尴尬局面(当时新生代高性能的 ParNew 只能选择拖后腿的 Serial Old 一起工作),现在 ParNew 也要陪着 CMS 一起下岗了。
综上所述,到 JDK 9 之后还剩下建议使用的组合如下
- G1
- Serial + Serial Old
- Parallel Scavenge + Serial Old (还是它,我一定要关注它俩啥时候下岗)
所以通过上面的分析,也能看出来 HotSpot 的用意,在 JDK 9 以后就是要将 G1 作为一个全能型的垃圾收集器来发展。
写在最后
上面总结了截止 JDK 9 的垃圾收集器内容,其实对于垃圾收集器还有很多内容,比如 Shenandoah ,一个由 Red Hat 开发的低延迟垃圾收集器,还有 Oracle 后面的 ZGC。这两个垃圾收集器都采用了更加优秀的思想和实现方案。不过因为我没有对其深入的了解,所以在这就不再多说了。如果你对垃圾收集器的具体算法仍感兴趣,推荐访问下面这个链接,R大 写的虚拟机相关内容
https://www.iteye.com/blog/rednaxelafx-362738
(正文完)