JVM的垃圾收集器经历了多代的发展,从单线程收集器到多线程收集器。
一、背景
垃圾回收器经历过多代的发展,从单线程到多线程,垃圾收集器的大家族产品如下,每一个连线表示可以组合使用。
接下来大概分为几个阶段详细介绍下各个垃圾回收器的特点:
阶段 | 收集器名称 | 区域 | 并行/串行/并发 | 算法 | 优缺点 | 适用场景 |
---|---|---|---|---|---|---|
第一阶段 | Serial | 新生代 | 串行 | 复制 | 响应速度快 | 单CPU环境下的Client模式 |
Serial Old | 老年代 | 串行 | 标志-整理 | 响应速度快 | 单CPU环境下的Client模式、CMS的后备预案 | |
第二阶段 | ParNew | 新生代 | 并行 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 新生代 | 并行 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | |
Parallel Old | 老年代 | 并行 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | |
第三阶段 | CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
第四阶段 | G1 | 新生代和老年代 | 并发 | 并行和并发 | 复制+标记-整理 | 响应速度优先 |
并行(Parallel):指多条垃圾收集线程线程并行工作,但此时线程用户线程仍然处于等待状态。
并行(Concurrent):指用户线程和垃圾收集线程同时执行,不一定是并行的,可能会交替执行,用户程序继续运行,而垃圾收集程序运行在另一个CPU上。
1.1 Serial和Serial Old(串行)收集器
Serial和Serial Old收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
1.2 ParNew(并行)收集器
ParNew收集器其实就是Serial的多线程版本,是并行收集器,ParNew与Serial Old的工作过程如下所示:
ParNew是使用-XX:+UseParNewGC进行显示启用, -XX:+UseConcMarkSweepGC开启 CMS的同时默认使用ParNew作为新生代收集器。
ParNew收集器在单CPU的 环境下绝不会超过Serial有更好的效果,随着CPU核数的增多,效果更明显,默认开启的收集线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads参数垃圾收集器的线程个数。
1.3 Parallel Scavenge和Parallel Old(并行)收集器
Parallel Scavenge收集器的目的达到一个可控制的吞吐量,CMS不同,CMS更关注缩短垃圾收集器收集时用户线程的停顿时间。
Parallel Scavenge提供两个参数设置停顿时间和吞吐率,-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间.
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n).
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
1.4 CMS(并发)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度,来提高用户的体验。
从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
收集器的过程分为4步骤:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
- 并发清除(CMS concurrent sweep)
CMS存在的缺点如下:
- 对CPU资源敏感。面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
- 无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。使用-XX:CMSInitiatingOccupancyFraction的值提高触发百分比,在JDK1.5的默认68%,在JDK1.6中,启动阈值已提高到92%。当CMS预留的内存无法满足程序需要时,就会出现一次“Concurrent Mode Failure”失败,这时候就会启动后备方案Serial Old收集器触发一次Full GC。
- 内存碎片问题。CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。这时候就会提前触发一次Full GC。为了 解决这个问题,-XX:UseCMSCompactAtFullCollection开关参数(默认是开启),如果CMS出现无法分配连续内存时,对碎片内存进行整理。-XX:CMSFullGCsBeforeCompation,默认值为0,表示每次进入FullGC之前执行多少次不压缩的GC后跟着执行一次碎片整理。
1.5 G1(并发)收集器
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,是一款面向服务端的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发: G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集: 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合: 与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿: 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1内部的逻辑将会开一篇文章详细介绍,敬请期待。