如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
这里讨论的收集器基于JDK 1.7 Update 14之后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于试验状态),这个虚拟机包含的所有收集器如图所示:
如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
并行(Parallel):指多个垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU。
1. Serial收集器
Serial收集器是最最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。大家看名字就会知道,这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
2. ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也公用了相当多的代码。ParNew收集器的工作过程如图所示:
3. Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器, 看上去和ParNew都一样,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughout)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确地控制吞吐量,分别是控制最大垃圾收停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使视同的垃圾收集速度变快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 / (1 + 19)),默认值为99,就是允许最大 1% (即1 / (1+99))的垃圾收集时间。
4. Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这收集器的主要意义是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK1.5以及之前的版本中与Paralllel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。Serial Old收集器的工作过程如图所示:
5. Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作过程如图所示:
6. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,系统系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
- CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束会有大量空间碎片产生。
7. G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方法让Java程序继续执行。
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
G1收集器运行过程示意图: