在前面,咱们已经了解了JVM的分代收集,知道JVM垃圾收集在新生代主要采用标记-复制
算法,在老年代主要采用标记-清除
和标记-整理
算法。接下来,咱们看一看JDK默认虚拟机HotSpot的一些垃圾收集器的实现。html
首先来看一下JDK 11以前所有可用的垃圾收集器。java
图中列出了七种垃圾收集器,连线表示能够配合使用,所在区域表示它是属于新生代收集器或是老年代收集器。算法
这里还标出了垃圾收集器采用的收集算法,G1收集器比较特殊,总体采用标记-整理
算法,局部采用标记-复制
算法,后面再细讲。多线程
Serial收集器是最基础、历史最悠久的收集器。并发
如同它的名字(串行),它是一个单线程工做的收集器,使用一个处理器或一条收集线程去完成垃圾收集工做。而且进行垃圾收集时,必须暂停其余全部工做线程,直到垃圾收集结束——这就是所谓的“Stop The World”。jvm
Serial/Serial Old收集器的运行过程如图:高并发
ParNew收集器实质上是Serial收集器的多线程并行版本,使用多条线程进行垃圾收集。布局
ParNew收集器的工做过程如图所示:性能
这里值得一提的是Par是Parallel(并行)
的缩写,但须要注意的是,这个并行(Parallel)
仅仅是描述同一时间多条GC线程协同工做,而不是GC线程和用户线程同时运行。ParNew垃圾收集也是须要Stop The World的。线程
Parallel Scavenge收集器是一款新生代收集器,基于标记-复制算法实现,也可以并行收集。和ParNew有些相似,但Parallel Scavenge主要关注的是垃圾收集的吞吐量。
所谓吞吐量指的是运行用户代码的时间与处理器总消耗时间的比值。这个比例越高,证实垃圾收集占整个程序运行的比例越小。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
-XX:MaxGCPauseMillis,最大垃圾回收停顿时间。这个参数的原理是空间换时间,收集器会控制新生代的区域大小,从而尽量保证回收少于这个最大停顿时间。简单的说就是回收的区域越小,那么耗费的时间也越小。
因此这个参数并非设置得越小越好。设过小的话,新生代空间会过小,从而更频繁的触发GC。
-XX:GCTimeRatio,垃圾收集时间与总时间占比。这个是吞吐量的倒数,原理和MaxGCPauseMillis相同。
因为与吞吐量关系密切,Parallel Scavenge收集器也常常被称做“吞吐量优先收集器”。
Serial Old是Serial收集器的老年代版本,它一样是一个单线程收集器,使用标记-整理算法。
Serial Old收集器的工做过程如图:
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,一样是老年代的收集齐,采用标记-清除
算法。
CMS收集齐的垃圾收集分为四步:
初始标记(CMS initial mark)
:单线程运行,须要Stop The World,标记GC Roots能直达的对象。并发标记((CMS concurrent mark)
:无停顿,和用户线程同时运行,从GC Roots直达对象开始遍历整个对象图。从新标记(CMS remark)
:多线程运行,须要Stop The World,标记并发标记阶段产生对象。并发清除(CMS concurrent sweep)
:无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象。涉及到了屡次标记的过程,这里插入一点
三色抽象
的知识。三色抽象用来描述对象在垃圾收集过程当中的状态。一般白色表明对象未被扫描到,灰色表示对象被扫描到但未被处理,黑色表示对象及其后代已被处理。在CMS的标记和清除过程当中就用到了这种抽象,详细的能够查看参考【5】。
Concurrent Mark Sweep收集器运行示意图以下:
优势
:CMS最主要的优势在名字上已经体现出来——并发收集、低停顿。
缺点
:CMS一样有三个明显的缺点。
Mark Sweep算法会致使内存碎片比较多
CMS的并发能力比较依赖于CPU资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,致使用户程序性能降低。
并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集没法处理浮动垃圾,必须到下一次垃圾收集才能处理。若是浮动垃圾太多,会触发新的垃圾回收,致使性能下降。
Garbage First(简称G1)收集器是垃圾收集器的一个颠覆性的产物,它开创了局部收集的设计思路和基于Region的内存布局形式。
虽然G1也还是遵循分代收集理论设计的,但其堆内存的布局与其余收集器有很是明显的差别。之前的收集器分代是划分新生代、老年代、持久代等。
G1把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region均可以根据须要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器可以对扮演不一样角色的Region采用不一样的策略去处理。
这样就避免了收集整个堆,而是按照若干个Region集进行收集,同时维护一个优先级列表,跟踪各个Region回收的“价值,优先收集价值高的Region。
G1收集器的运行过程大体可划分为如下四个步骤:
初始标记(initial mark),标记了从GC Root开始直接关联可达的对象。STW(Stop the World)执行。
并发标记(concurrent marking),和用户线程并发执行,从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象、
最终标记(Remark),STW,标记再并发标记过程当中产生的垃圾。
筛选回收(Live Data Counting And Evacuation),制定回收计划,选择多个Region 构成回收集,把回收集中Region的存活对象复制到空的Region中,再清理掉整个旧 Region的所有空间。须要STW。
相比CMS,G1的优势有不少,能够指定最大停顿时间、分Region的内存布局、按收益动态肯定回收集。
只从内存的角度来看,与CMS的“标记-清除”算法不一样,G1从总体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,不管如何,这两种算法都意味着G1运做期间不会产生内存空间碎片,垃圾收集完成以后能提供规整的可用内存。
在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。
与CMS中的ParNew和G1相似,ZGC也采用标记-复制算法,不过ZGC对该算法作了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键缘由。
ZGC虽然在JDK 11还处于实验阶段,但因为算法与思想是一个很是大的提高,将来前景相信仍是很广阔的。
垃圾收集器的选择须要权衡的点仍是比较多的——例如运行应用的基础设施如何?使用JDK的发行商是什么?等等……
这里简单地列一下上面提到的一些收集器的适用场景:
设置垃圾收集器(组合)的参数以下:
新生代 | 老年代 | JVM 参数 |
---|---|---|
Incremental | Incremental | -Xincgc |
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseParallelOldGC |
Parallel New | Serial | N/A |
Serial | Parallel Old | N/A |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | Parallel Old | N/A |
Serial | CMS | -XX:-UseParNewGC -XX:+UseConcMarkSweepGC |
Parallel Scavenge | CMS | N/A |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
参考:
【1】:周志朋编著《深刻理解Java虚拟机:JVM高级特性与最佳实践》
【2】:《垃圾回收算法手册 自动内存管理的艺术》
【3】:Garbage Collection in Java – What is GC and How it Works in the JVM