之前我们了解了垃圾回收算法,现在一起来看看垃圾回收的具体实现:垃圾收集器。Java虚拟机规范对于垃圾收集器应该如何实现没有具体的规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器有可能有较大的区别。以下列出的是基于JDK 1.7 Update 14之后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器):
上图展示了7种不同分代的收集器,如果两个收集器中存在连线,就说明它们之间可以搭配使用。虚拟机所处的区域,则说明该虚拟机是属于新生代收集器还是老年代收集器。其中属于新生代的收集器有Serial、ParNew、Parallel Scavenge,属于老年代的收集器有CMS、Serial Old、Parallel Old。而G1收集器在新生代和老年代中都可以用。
需要强调的是,到目前为止还没有最好的垃圾收集器出现,更加没有万能的收集器,所以我们选择的是对具体应用最合适的收集器,一般来说是两种收集器搭配起来使用。
1、Serial收集器- 最基本的、历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。
- 单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到垃圾收集结束,“Stop The World”的说法就是因此而得来的,但如果垃圾收集时间过长,就会带来程序停顿的不良体验。
- Client模式下的默认新生代收集器,相比其他收集器,它简单而高效,对于单个CPU的环境来说,由于没有线程交互的开销,自然可以获得最高的单线程收集效率。
Serial/Serial Old收集器的运行示意图如下:
2、ParNew收集器- Serial收集器的多线程版本,也是新生代收集器。
- 除了多线程进行垃圾收集外,其余的行为包括可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,实际上,它们两也共用了相当多的代码。
- Server模式下虚拟机中首选的新生代收集器,主要原因是出了Serial收集器外,只有它能与CMS收集器配合工作。使用-XX: UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX: UseParNewGC来强制使用它。
- ParNew收集器在单CPU环境中垃圾回收效率比不上Serial收集器,但随着CPU数量的增加,它的回收性能会逐渐超过Serial收集器。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
ParNew/Serial Old收集器的运行示意图如下:
3、Parallel Scavenge收集器- 新生代、使用复制算法、并行的多线程收集器。
- 它的关注点与其他收集器不同,目的就是达到一个可控制的吞吐量(Throughput),因此也被称为“吞吐量优先”收集器。吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 垃圾收集时间)。比如:虚拟机总共运行了100分钟,其中垃圾收集用了一分钟,那么吞吐量就是99%。
- 提供了两个参数用于精准控制吞吐量,分别是-XX:MaxGCPauseMillis(控制最大垃圾收集停顿时间)和-XX:GCTimeRatio(直接设置吞吐量大小)。
- 通过使用-XX: UseAdaptiveSizePolicy打开开关,就不需要手工指定-Xmn、-XX:SurvivorRatio、-XX:PretenureSizeThreshold等细节参数了,虚拟机会根据系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种方式成为GC自适应的调节策略(GC Ergonomics)。自适应策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
Parallel Scavenge/Parallel old收集器的运行示意图如下:
4、Serial Old 收集器- Serial收集器的老年代版本,它同样是单线程收集器,使用“标记-整理”算法。
- 主要给client模式下的虚拟机使用。
- 如果在Server模式下,它只要有两大用途:一是在JDK1.5之前与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。
Serial/Serial Old收集器的运行示意图如下:
5、Parallel Old 收集器- Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
- 在JDK1.6后才提供,直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了名副其实的应用组合,在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
Parallel Scavenge/Parallel old收集器的运行示意图如下:
6、CMS收集器- CMS(Concurrent Mark Sweep)收集器目标就是获取最短回收停顿时间,因此很符合注重响应速度的系统。
- 基于“标记—清除”算法实现的,运行过程有四个步骤:
- 初始标记(CMS initial Mark):仅仅只是标记GC Roots能关联到的对象,速度很快。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间比初始阶段稍长,但远比并发标记的时间短。
- 并发清除(CMS concurrent sweep)。
- 其中初始标记和重新标记需要“Stop The World”。整个过程中耗时最长的并发标记和并发清除过程可以与用户线程一起工作,因此从总体上来说CMS收集器内存回收过程是与用户线程一起并发执行的。
CMS收集器的运行过程如下:
CMS收集器的优点:
由于以上特点,CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。
但CMS也不是完美,它有三个明显的缺点:
- 对CPU资源敏感。CMS默认启用的回收线程数是(CPU数量 3)/ 4,也就是说在CPU为4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。当CPU不足4个时,CMS收集器对用户程序的影响就会变大。
- 无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”从而导致另一次Full GC的产生。
- 由于基于“标记—清除”算法,会产生大量的空间碎片,将导致无法找到足够大的连续空间来分配对象从而提前触发Full GC。
7、G1收集器 G1(Garbage-First)收集器是当今收集器技术发展的最前源成果之一,直到JDK7u4,才开始用于商用。
G1是一款面向服务器应用的垃圾收集器,HotSpot团队准备用它来代替CMS收集器。与其他收集器相比,G1收集器具有以下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多CPU来缩短Stop The World的时间。
- 分代收集:与其他收集器一样,分代收集概念在G1中任然保留着。虽然G1不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记—清理”算法不同,G1从整体上看是基于“标记—整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。采用以上这两种算法,意味着无论如何也不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续的内存空间而提前触发GC。
- 可预测的停顿:这是G1相比于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1出了关注低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前其他收集器进行收集的范围都是整个新生代或者老年代,而使用G1时,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以可以建立可预测的停顿时间模型,是因为它有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。这种使用Region划分内存空间已经有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能的收集效率。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remember Set来避免全堆扫描的,
如果不计算维护Remember Set的操作,G1收集器的运行大致可以分为以下四个步骤,这些过程与CMS有许多相似之处:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象,这阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking):为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remember Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
通过下图可以清楚的看到G1收集器运行过程:
由于G1成熟版本的发布时间还很短,没有经过实际应用的考验,网上关于G1收集器的性能测试也非常贫乏,如果原来采用垃圾收集器比如CMS没有出现问题,我们没有理由去选择G1,如果应用追求低停顿,那么G1可以作为一个可尝试的选择。相信随着Oracle对G1的持续改进,G1会成为最终的胜利者。
参考:
深入理解Java虚拟机第3章-周志明