前面介绍了垃圾回收算法,但是 JVM 如何根据这些算法进行内存回收呢?因为内存回收如何进行是由虚拟机所采用的GC收集器决定的,而通常虚拟机中往往不止有一种GC收集器。下面继续来看HotSpot中有哪些GC收集器。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。HotSpot虚拟机包含的所有收集器如下:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
一、Serial 收集器
Serial收集器为新生代单线程收集器,使用复制算法。在进行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束。运行过程如下图所示:
说明:1. 需要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
二、ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PrctenureSizcThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。运行过程如下图所示:
说明:1.Server模式下虚拟机的首选新生收集器,与CMS进行搭配使用。
三、Parallel Scavenge 收集器
Parallel Scavenge收集器是一个新生代收集器,它也使用复制算法,又是并行的多线程收集器,目标是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,
另外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy
值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX: SurvivorRatio )、晋升老年代对象年龄(-XX:PretenureSizeThreshold )等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略。
四、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记一整理”算法。运行过程如下图所示:
五、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。运行过程如下图所示:
六、CMS收集器
CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
- 初始标记,标记GCRoots能直接关联到的对象,时间很短;
- 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
- 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
- 并发清除,回收内存空间,时间很长。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:
CMS还远达不到完美的程度,它有以下3个明显的缺点:
对CPU资源非常敏感,会因为占用一部分线程而导致应用程序变慢,总吞吐量下降。
无法处理浮动垃圾。因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾。同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。
由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FuIIGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发
的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX: CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进人Full GC时都进行碎片整理)。
七、G1收集器
可以在新生代和老年代中只使用G1收集器。具有如下特点:
- 并行和并发:使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
- 分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
- 空间整合:基于标记 - 整理算法,无内存碎片产生。
- 可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆会被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但两者已经不是物理隔离了,都是一部分Region(不需要连续)的集合。
在G1收集器中,Region之间的对象引用以及其他收集器的新生代和老年代之间的对象引用,虚拟机都使用Remembered Set来避免全堆扫描的。每个Region对应一个Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查老年代的对象是否引用了新生代的对象),如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作可以分为如下几步:
- 初始并发,标记GCRoots能直接关联到的对象;修改TAMS(Next Top At Mark Start),使得下一阶段程序并发时,能够在可用的Region中创建新对象,这阶段需停顿线程,但耗时很短。
- 并发标记,从GCRoots开始进行可达性分析,与用户程序并发执行,耗时很长。
- 最终标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,变动的记录将被记录在Remembered Set Logs中,此阶段会把其整合到Remembered Set中,需要停顿线程,与用户程序并行执行,耗时较短。
- 筛选回收,对各个Region的回收价值和成本进行排序,根据用户期望的GC时间进行回收,与用户程序并发执行,时间用户可控。
G1收集器具体的运行示意图如下: