没有一款放之四海而皆准的完美的垃圾收集器,每个垃圾收集器都有自己的优缺点,所以HotSpot虚拟机选择了不同的垃圾收集器来进行垃圾收集操作。
其中新生代的垃圾收集器,如Serial,ParNew,Parallel Scavenge
老年代的垃圾收集器,CMS,Serial Old,Parallel Old
整堆的垃圾收集器,G1
其中连线表示可以搭配使用。
下面是详细的介绍。
1. Serial
最大的特点是单线程,但是这里的单线程的意思是,当Serial进行垃圾回收的时候将会停止所有的用户线程。
想象一下Serial为了自己的GC 线程运行而停止应用程序线程的窘境,想象一下每到safePoint就要卡住的尴尬。
但是即便如此,Serial还是在当前的虚拟机列表上(气不气人),它依然是Client模式下额默认新生代收集器,因为单线程的简单而高效,没有线程交互的开销。
2. ParNew
ParNew就是Serial的多线程版本,在新生代有多条GC线程同时运行,而在老年代,只有一条GC线程运行。
实际上,Serial和ParNew的好多代码也是一样的。
看起来ParNew和Serial的差距仅仅在于线程数量上,但是在Server模式下的虚拟机中ParNew的重要性就显现出来了,我们可以看到开篇的连线,我们说,带连线的可以配合使用,其中老年代一款CMS的收集器,与新生代的Serial,ParNew可以配合使用,而CMS是一款突破性的收集器,做到了可以让垃圾回收线程和用户线程同时工作(系统层面的调度就不谈了,我们只说达到的类似的效果)。
既然有CMS这样的收集器,如果在新生代还用Serial配合的话,那性能就大打折扣了,所以ParNew还是很自然的站稳了多线程的优势地位。
3. Parallel Scavenge
Parallel是一个新生代的垃圾收集器,也是使用复制算法的收集器,又是并行的多线程收集器,但它到底与ParNew差别在哪里呢?
Parallel收集器的关注点与其他收集器不同,CMS的关注点主要在缩短垃圾收集时用户线程的停顿时间,而Parallel的关注点是达到一个可控的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
两个各自有各自的场景,比如说CMS能提供良好的响应速度,这样就可以提升用户的体验,而高的吞吐量就可以高效率的利用CPU时间,尽快的完成程序的运算任务,主要适合后台运算不需要太多交互的任务。
4 . Serial Old
单线程,老年代收集器,运行的算法是标记整理。
Client模式下虚拟机使用本收集器,此外在Server模式下,还有两大用途,一是Jdk1.5以及之前的版本中与Parallel Scavenge搭配使用,另一种用途是作为CMS的后被预案,在并发收集发生Concurrent Mode Failure时使用。
5. Parallel Old
Parallel Old是Parallel Scavenge的老年代版本,使用多线程 标记整理算法,在Jdk1.6提供,在此之间Parallel Scavenge只能跟Serial Old搭配,由于Serial Old的单线程特性,所以很大限度的限制了Parallel Scavenge的性能,后来就出现了Parallel Old版本。所以在服务端注重吞吐量的场合,Parallel Scavenge和Parallel Old是名副其实的组合。
6. CMS (Concurrent Mark Sweep)
前面提到过,CMS是一款为了获取最短回收停顿时间为目标的收集器,从名字可以看出,是基于标记清除算法来实现的。
主要分为下面几个步骤:
- 初始标记(CMS inital mark):需要“stop the world”,但只标记一下GC Roots能直接关联的对象,速度很快。
- 并发标记(CMS concurrent mark):是GC Roots Tracing的过程,花费时间长
- 重新标记(CMS remark):需要“stop the world”,是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除(CMS concurrent sweep):是并发清除无用对象。
但是CMS也有相应的缺点,如下:
CMS对CPU资源非常敏感,由于CMS是面向并发设计的,当在并发阶段,虽然不会导致用户线程停顿,但是会因为占用CPU资源导致应用程序变慢,总吞吐量降低,
CMS无法处理浮动垃圾,可能会出现 “Concurrent Mode Failure”失败而导致另一次Full GC的发生。在CMS的并发清理阶段,由于程序还在运行,垃圾还会不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留到下一次GC再处理。这种垃圾称为浮动垃圾。同样由于CMS GC阶段用户线程还需要运行,即还需要预留足够的内存空间供用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被灌满了再进行收集而需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现concurrent mode failure,这时候就会启用Serial Old收集器作为备用进行老年代的垃圾收集。我们之前有提到过后备方案。
前面所提到过的GC 碎片问题,由于CMS是基于标记清除实现的,所以难免会有碎片问题产生。
7. G1
G1收集器(Garbage-First):是当今收集器技术发展的最前沿的成果之一,G1是一款面向服务器端应用的垃圾收集器。 使用G1收集器时,java堆的内存布局就与其他收集器有很大差别,它将整个java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代与老年代的概念,但新生代与老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。
可以看到上图,Eden,Survivor,Old Generation已经不是严格意义上的分层了,这样更加提供了内存的灵活性。
G1的特点:
- 并行与并发:同样类似于CMS,G1也会在多核,多CPU的条件下达到一个并行与并发的效果来缩短停顿时间。
- 分代收集:可以参考上图,G1虽然可以独自处理整个GC堆,不需要其他收集器配合,但是也没有取消分代的概念,并且还采用了如上图所示的分代方法来处理GC。
- 空间整合:上面提到过CMS的缺点,Mark-Sweep的使用使得CMS产生碎片多的情形,而G1的Region方案使得整体类似于Mark-Compact,而局部类似于Copy方案,所以无论如何,都不会产生Sweep方案的尴尬。
- 可预测的停顿:这一点属于G1的一大优势,前面说到过,G1和CMS共同的特点就是追求短的停顿时间,而G1作为新的收集器,不仅能有短停顿,而且可以建立可预测的停顿时间模型。
G1的工作过程也分为四步:
1. 初始标记
标记GC Roots能够直接关联到的对象,并且修改TAMS的值,能在正确可用的Region中创建对象,这阶段需要停顿线程,而且耗时很短。
2. 并发标记
从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这个时间耗时比较长,但可与用户程序并行执行。
3. 最终标记
为了修正和正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分没有标记记录,虚拟机将这一段对象变法记录在线程Rememberred Set logs里面,最终标记阶段需要把Remembered Set logs 的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并发执行。
4. 筛选回收
对各个Region的回收截止和成本进行排序,根据用户期望的GC停顿时间来制定回收计划,这阶段可以做到和用户程序一起并发执行,但是因为值回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
以上是几种垃圾回收器的简单介绍,路漫漫……接着深入吧。