一、7种垃圾收集器的详细介绍
上图展示了7种收集器,有连线的两个收集器可以互相搭配使用。它所处的区域则表示它是属于新生代收集器还是老年代收集器,新生代中的收集器多采用复制算法,老年代中的收集器多采用标记-整理算法。
1. Serial收集器
特点:
(1) 是单线程收集器,它不仅仅只使用一个CPU或者一条线程完成GC,而且在它进行GC时,必须暂停其他所有的工作线程,直到它收集结束。(就好像你的计算机每运行1个小时就要暂停响应5分钟)
(2) 优点是简单高效,在限定单个CPU的环境下,由于没有线程交互的开销,专心做GC的效率很高。
2. ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了线程数以外,两者的其余行为都完全一样。除了Serial收集器以外,目前只有ParNew收集器能与CMS配合工作,因此它也是许多Server模式下新生代收集器的首选。
Ps:从这开始,后面还会遇到几款并发与并行的收集器,所以解释下两个概念。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待;
并发(Concurrent):指用户线程与垃圾收集线程同时执行(不一定是并行,可能是交替执行),用户程序在继续运行,而垃圾收集程序云星宇另一个CPU上。
3. Parallel Scavenge收集器
(1) 特点:
新生代收集器,复制算法,并行收集,面向吞吐量要求(吞吐量优先收集器)。所谓吞吐量 = 用户代码运行时间/(用户代码运行时间+垃圾回收时间)。
(2) 使用场景:
主要适合在后台运算而不需要太多交互的任务。高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务等。
(3) 三个参数:
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于零的毫秒数。
-XX:GCTimeRatio:0到100的整数,垃圾收集时间占总时间的比例,相当于吞吐量的倒数。
-XX:UseAdaptiveSizePolicy:打开之后虚拟机根据系统运行状况,调整停顿时间,吞吐量, 称作GC自适应调节策略,这也是它与ParNew收集器的一个重要区别。
4. Serial Old收集器
是Serial收集器的老年代版本,主要给Client模式下的虚拟机使用。如果在Server模式下,它有两种用途:
(1) 在JDK1.5以前与Parallel Scavenge搭配使用;
(2) 作为CMS收集器的后备方案,在并发收集失败时使用。
5. Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在该收集器出现以前,当新生代选择了Parallel Scavenge收集器,则老年代只能选择Serial Old(因为Parallel Scavenge收集器与CMS不能配合使用),这样的组合不如ParNew + CMS的组合给力。
直到Parallel Old收集器的出现,则可以选择Parallel Scavenge + Parallel Old,在注重吞吐量的场合使用。
6. CMS收集器
这是一种以获取最短回收停顿时间为目标的收集器,从CMS(Concurrent Mark Sweep)的名字上来看,它是基于“标记-清除”算法实现的。
运作过程分为4个阶段:
(1) 初始标记。
暂停所有的其他线程,并记录下直接与GC root相连的对象,速度很快。
(2) 并发标记
同时开启GC和用户线程,并进行GC roots Tracing的过程。这个过程中,因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性,无法保证包含当前所有的可达对象。
(3) 重新标记
暂停所有的其他线程,为了修正并发标记阶段由于用户线程的运作而发生变动的引用域。这个阶段的停顿时间一般比初始标记阶段稍长,但是远比并发标记的短。
(4) 并发清除
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个过程要注意不要清扫了刚被用户线程分配的对象。
优点:并发收集、低停顿
缺点:
1 对CPU资源非常敏感。在收集过程中会大量占用CPU的时间,总吞吐量低。
2 无法处理浮动垃圾。由于CMS并发清理阶段用户线程还在运行着,伴随程序自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理。
3 产生大量空间碎片。CMS是基于“标记-清除”算法实现的,这意味着收集结束时会产生大量空间碎片。CMS提供了一个参数可以进行内存碎片的整理过程,但是这个过程无法并发,只能牺牲停顿时间来解决碎片问题。另一个参数可以设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。
7. G1收集器(Garbage-First)
特点:
(1) 并行与并发。
(2) 分代收集。G1可以独立管理整个GC堆,能采用不同的方式处理新创建的对象和已经存活了一段时间的旧对象。
(3) 空间整合。G1从整体上看是基于“标记-整理”算法,从局部(两个Region之间)上看是基于“复制”算法实现,这两种算法都不会产生内存空间碎片。
(4) 可预测的停顿。能建立可预测的停顿时间模型,让使用者明确指定再一个M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。
使用G1时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离,他们都是一部分Region(不需要连续)的集合。G1不需要在整个Java堆中进行全区域的GC,它是跟踪各个Region中垃圾堆积的价值大小(回收能获得的空间大小以及回收所需时间的比重),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First名字的由来)。这保证了G1在有限的时间内可以获得尽可能高的收集效率。
G1如何避免进行全堆扫描?
在G1中,Region之间的对象引用,或者其他收集器中新生代和老年代之间的对象引用,虚拟机都通过Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,当虚拟机发现程序正在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代的例子中就是检查老年代的对象是否引用了新生代中的对象),如果是,那么通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set,这样就可以保证不对全堆进行扫描也不会遗漏。
G1的运作过程:
1. 初始标记。标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这个阶段需要停顿线程,但耗时很短。
2. 并发标记。从GC Root出发开始对堆中对象进行可达性分析,找出存活对象。这个阶段耗时较长,但可与用户程序并发执行。
3. 最终标记。为了修正并发标记期间因用户程序继续运作而导致标记产生变动,把这些变动记录在线程Remembered Set Logs中,并把Remembered Set Logs的数据合并到Remembered Set中。这个阶段需要暂停线程,但是可并行执行。
4. 筛选回收。先对各个Region的回收价值排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段可以做到并发,但暂停用户线程可大幅提高效率。