1.1 Serial 收集器(Client模式)
Serial收集器是一种单线程垃圾收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。也就是之前提到的”Stop the world“。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。Serial/Serial Old收集器的工作示意图如下:
尽管有以上不能让人接受的地方,但实际上到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。它有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
1.2 ParNew 收集器(Server模式)
ParNew收集器是Serial收集器的多线程版本,ParNew收集器的工作示意图如下:
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
但是,在单CPU环境中,ParNew收集器绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。
1.3 Parallel Scavenge 收集器(Server模式,吞吐量)
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
它的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
- -XX:GCTimeRatio:直接设置吞吐量大小
直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
除此之外,Parallel Scavenge收集器还可以设置参数-XX:+UseAdaptiveSizePocily来动态调整停顿时间或者最大的吞吐量,这种方式称为GC自适应调节策略,这点是ParNew收集器所没有的。
1.4 Serial Old 收集器(Client模式)
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
1.5 Parallel Old 收集器(Server模式)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用 Parallel Scavenge + Parallel Old 组合。
1.6 CMS 收集器(回收停顿时间)
CMS收集器(Concurrent Mark Sweep)的目标就是获取最短回收停顿时间。在注重服务器的响应速度,希望停顿时间最短,则CMS收集器是比较好的选择。基于“标记-清除”算法实现。
整个执行过程分为以下4个步骤:
- 初始标记:标记GC Roots能直接关联到的对象,速度很快
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
-
并发清除
由上图可知,初始标记、重新标记这两个步骤仍然需要“Stop The World”。 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:
并发收集、低停顿。也被称为“并发低停顿收集器”。由于进行垃圾收集的时间主要耗在并发标记与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的。
缺点:
- CMS收集器对CPU资源非常敏感。在并发阶段,CMS占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4。CPU数量越少,回收线程所占资源就越多,对用户程序影响越大。为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”,它使得在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。
- CMS收集器无法处理浮动垃圾。所谓的“浮动垃圾”,就是在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。
- 需要预留内存。由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活。在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
- CMS是一款基于“标记—清除”算法实现的收集器。会产生内存碎片空间。碎片空间过多就会对大对象的分配空间造成麻烦。为了解决碎片问题,CMS提供一个参数来控制是否在GC的时候整合内存中的碎片,这个碎片整合的操作是无法并发的,会延长STW的时间。
1.7 G1 收集器(Server模式)
G1的特点:
- 并行与并发:利用多CPU来缩短STW的时间
- 分代收集:可以独立管理整个堆(使用分代算法)
- 空间整合:整体是基于标记-整理,局部使用复制算法,不会产生碎片空间
- 可以预测停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1把整个java堆分成多个Region,然后计算每个Region里面的垃圾大小(根据回收所获得的空间大小和回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
G1的运行过程:
- 初始标记:标记一下GC ROOT能直接关联的对象,速度很快,这个阶段是会STW。
- 并发标记:在GC ROOT中运用可达性分析算法,找出存活的对象,耗时较长,但是无需STW。
- 最终标记:修正并发标记期间用户线程对垃圾对象的修改,需要停顿线程,但是可以并行执行。
- 筛选回收:先计算回收各个Region的价值,然后根据用户需求来进行回收。
总结
收集器 | 垃圾收集算法 | 年代 | 面向对象 |
---|---|---|---|
Serial | 复制算法 | 新生代 | Client |
ParNew | 复制算法 | 新生代 | Server |
Parallel Scavenge | 复制算法 | 新生代 | Server |
Serial Old | 标记-整理算法 | 老年代 | Client |
Parallel Old | 标记-整理算法 | 老年代 | Server |
CMS | 标记-清除算法 | 老年代 | Server |
G1 | 整体:标记-整理算法,局部:复制算法 | 整个Java堆 | 服务器 |