关于垃圾收集器你了解多少?一文总结七大垃圾收集器
听说微信搜索《Java 鱼仔》会变更强哦!
本文收录于JavaStarter ,里面有我完整的 Java 系列文章,学习或面试都可以看看哦
(一)概述
如果说垃圾收集算法是内存回收的理论,那么垃圾收集器就是内存回收的具体实现。
垃圾收集器目前存在的有很多,但是依旧没有哪个收集器是万能的存在,我们只能选择一个最适合应用的收集器。
下面会介绍目前主流 Java 虚拟机中所采用的七种垃圾收集器:
Serial、parNew、ParallelScavenge、SerialOld、ParallelOld、CMS、G1
上述垃圾收集器有些适用于新生代,有些适用于老年代,有些在新生代和老年代都适应。如下图所示,连线表示可以配合使用。
(二)Serial
Serial 是一个单线程的收集器,Serial 的特点是它在进行垃圾收集时,必须“Stop the World”,意思就是当这个垃圾收集器开始工作时,必须停止其他所有的工作线程。听起来似乎很不靠谱,但是对于限定单个 CPU 的场景下,这种方式简单而高效。对于简单的桌面应用,分配给虚拟机的内存不会很大,对于一两百兆的新生代,Serial 的垃圾收集时间可以控制在一百毫秒以内,对于用户来说基本上是无影响的。
Serial 收集器在新生代使用复制算法。
(三)ParNew
ParNew 垃圾收集器是 Serial 的多线程版本,使用多条线程进行垃圾收集。除此之外,和 Serial 基本相同,ParNew 在多线程收集垃圾时依旧需要“Stop the World”。ParNew 可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数量。
ParNew 收集器在新生代使用复制算法
(四)Parallel Scavenge
Parallel Scavenge 也是新生代收集器,也同样是多线程的收集器,但是和 ParNew 不同,Parallel Scavenge 收集器关注的是一个可控制的吞吐量(Throughput)。所谓吞吐量指的是 CPU 用于运行代码的时间和 CPU 总消耗的时间比例。
吞吐量=运行代码的时间 /(运行代码的时间+垃圾收集时间)
理论上吞吐量越高,用户就越不能感受到停顿时间。
Parallel Scavenge 提供了两个参数用来控制吞吐量:
-XX:MaxGCPauseMillis 和*-XX:GCTimeRatio*
-XX:MaxGCPauseMillis 设置内存回收花费时间最高毫秒值,但是不要一味地认为只要把值设置很小,垃圾回收就更快了。这个停顿时间是以牺牲吞吐量和新生代空间换来的。
-XX:GCTimeRatio 表示垃圾收集时间占总时间的比例,(1~100),也就是吞吐量的倒数。默认这个值是 99,就是允许最大百分之 1 的垃圾手机时间(1/(1+99))。
还有一个参数-XX:+UseAdaptiveSizePolicy,打开这个参数后,就不需要自己设置新生代大小、晋升老年代对象年龄等参数,因此 Parallel Scavenge 收集器也被叫做吞吐量优先垃圾收集器。
Parallel Scavenge 采用复制算法。
(五)Serial Old
一听名字就知道这是 Serial 收集器的老年代版本,是单线程收集器,采用标记-整理算法,其余的和新 Serial 基本相同。
(六)Parallel Old
Parallel Scavenge 收集器的老年版本,多线程收集器,采用标记-整理算法,也是吞吐量优先。
(七)CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 是基于标记-清除算法的老年代垃圾回收器,CMS 是目前应用最广泛的老年代垃圾回收器,它进行垃圾回收分为以下四步:
1、初始标记:标记 GC Roots 可以直接关联到的对象,速度很快(stop the world)
2、并发标记:根搜索算法的过程
3、重新标记:为了修正并发标记期间,因程序运行导致标记产生变动的对象。(stop the world)
4、并发清除:清除垃圾
这个过程中耗时最长的是并发标记和并发清除的过程,但是并不会 stop the world,而初始标识和重新标记的速度都很快,即使 stop the world 也不会占用太多时间。
它的优点就是并发收集、并发清除、低停顿。
但是它有三个显著的缺点:
1、对 CPU 资源十分敏感,因为并发标记和并发清除都是和程序同时运行,因此会占用 CPU 导致应用程序变慢。
2、无法处理浮动垃圾,浮动垃圾就是在并发清除过程中新生成的垃圾,这部分垃圾 CMS 无法在本次被清理,可能出现 Concurrent Mode Failed 报错,因此需要预留一定的内存空间,无法等到老年代快被占满时再清除。默认情况下,CMS 在老年代使用了 68%后就会被激活。可以设置-XX:CMSInitiatingOccupancyFraction 设置这个值。
3、产生空间碎片,由于采用的是标记-清除算法,那就无法避免会产生空间碎片的问题,这会给分配大对象带来困难。
(八)G1
上面的垃圾回收器基本上都是按新生代和老年代去区分,但是 G1 不一样
堆结构
G1 的堆结构就是把一整块内存区域划分为多个固定大小的块,JVM 一般把堆划分为 2000 个 region,然后每个 region 从 1M 到 32M 不等。
内存的分配
所有的 region 会被划分为 Eden、Survivor、Old 和 Humongous,其中对 Eden、Survivor 和 Old 的理解用其他垃圾回收器去理解,这里多了一种类型 Humongous,这个类型主要用来存储比标准块大百分之 50 或者更大的对象。
G1 中的 YGC
第一次 YGC 时,Eden 块中存活的对象会被转移到一个或多个 survivor 块中,存活时间达到阈值,这些对象就会晋升到老年代。年轻代 GC 通过多线程并行进行。
此时会有一次 stop the world 暂停,会计算出 Eden 大小和 survivor 大小,用于下次 young GC。统计信息会被保存下来,用于辅助计算 size。比如暂停时间之类的指标也会纳入考虑。
一旦发生一次新生代回收,整个新生代都会被回收(根据对暂停时间的预测值,新生代的大小可能会动态改变)
G1 中的老年代垃圾收集
老年代回收不会回收全部老年代空间,只会选择一部分收益最高的 Region,回收时一般会搭便车——把待回收的老年代 Region 和所有的新生代 Region 放在一起进行回收,这个过程一般被称为 Mixed GC
G1 中的老年代垃圾收集和 CMS 收集器很相似
1、初始标记:附加在正常的 YGC 过程中,标记所有的根。(stop the world)
2、扫描根区域:扫描 Survivor Regions 中指向老年代的被初始标记标记的引用及引用的对象,这个阶段是并发执行的,但是在年轻代 GC 发生之前必须完成。(stop the world)
3、并发标记:在整个堆中查找活着的元素,此阶段可被 YGC 打断
4、再次标记:类似 CMS 的重新标记,处理并发标记阶段产生的新的对象引用,这阶段使用了 SATB(snapshot-at-the-beginning)算法,该算法比 CMS 中所采用的快很多。(stop the world)
5、清理阶段:G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域进行清理。(stop the world)
你可以发现,有四个阶段都需要 stop the world,为了降低 stop the world 的时间,G1 使用了 RSet(Remembered Set)来记录不同代之间的引用关系。
RSet
RSet 记录了“谁引用了我”,RSet 记录了以下两种引用:
1、老年代 Region 间的引用
2、老年代 Region 到新生代 Region 的引用,Young GC 时直接将这种引用加入 GC Roots。
RSet 的工作原理是这样的,进行 Young GC 时,选择新生代所在的 Region 作为 GC Roots,这些 Region 中的 RSet 记录了老年代->新生代的的跨代引用(「谁引用了我」),从而可以避免了扫描整个老年代。进行 Mixed GC 时,「老年代->老年代」之间的引用,可以通过待回收 Region 中的 RSet 记录获得,「新生代->老年代」之间的引用通过扫描全部的新生代获得(前面提到过 Mixed GC 会搭 Young GC 的便车),也不需要扫描全部老年代。总之,引入 RSet 后,GC 的堆扫描范围大大减少了。
(九)总结
上面这七种垃圾收集器中最优秀的非 G1 莫属,但是它还不够好,第一个原因是 RSet 会占用一定的内存,第二个原因是 stop the world 时间太长了。目前有两款更新的垃圾回收器:ZGC/C4 垃圾回收器、*Shenandoah 垃圾回收器*,有兴趣的小伙伴可以去了解下。
版权声明: 本文为 InfoQ 作者【Java鱼仔】的原创文章。
原文链接:【http://xie.infoq.cn/article/ea0ff8bc3c14612db2d78b4f2】。文章转载请联系作者。
评论