如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
HotSpot JVM中GC的实现主要有以下的几种:
Serial/Serial Old
ParNew
Parallel Scavenge/Parallel Old
Concurrent Mark Sweep(CMS)
Garbage First(G1)
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
7种Java垃圾收集器,错误的选择会对性能影响很大,不同的选择会导致应用的吞吐量(throughtput)和停顿(pause)有巨大的不同。
1. 串行收集器Serial/Serial Old
Serial收集器:新生代收集器,单线程收集器,串行,使用复制算法,其它工作线程暂停。
Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记-清理-压缩算法,其它工作线程暂停(在老年代中进行标记整理算法清理,也需要暂停其它线程)
2. ParNew收集器
ParNew收集器:新生代收集器,是Serial收集器的多线程版本,采用复制算法,多线程进行收集。
ParNew比较重要,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它可以配合CMS收集器一起使用(Parallel Scavenge则不行)。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
3. 并行/吞吐优先收集器Parallel Scavenge/Parallel Old
Parallel Scavenge:新生代收集器,多线程收集器,使用复制算法,它与ParNew最主要的区别是它的目标是吞吐量优先而不是时间优先(两者不能兼得)。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU运行总时间的比值。吞吐量优先适合在后台完成计算而不需要太多交互的业务,而时间优先适合需要交互和实时性的业务。
比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算
关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适合用户交互,提高用户体验
Parallel Old:老年代收集器,多线程,并行,使用标记-整理(与Serial Old不同,这里的整理是汇总和压缩,汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep那样清理废弃的对象)算法,目标也是吞吐量优先,可以与Parallel Scavenge结合。
4. CMS收集器CMS Collector
CMS是真正意义上的并发收集器,作用于老年代。CMS的目标是时间优先(最短停顿时间),像服务器之类的就很适合跑在CMS收集器下,因为互联网服务重视服务的响应速度,希望系统延迟时间短。CMS通常与ParNew配合使用。
CMS是基于标记-清除算法实现的,整个过程分几步:
初始标记(initial-mark):从GC Root开始,仅扫描与根节点直接关联的对象并标记,这个过程需要STW,但是GC Root数量有限,因此时间较短
并发标记(concurrent-marking):这个阶段在初始标记的基础上继续向下进行遍历标记。这个阶段与用户线程并发执行,因此不停顿
并发预清理(concurrent-precleaning):上一阶段执行期间,会出现一些刚刚晋升老年代的对象,该阶段通过重新扫描减少下一阶段的工作。该阶段并发执行,不停顿
重新标记(remark):重新标记阶段会对CMS堆上的对象进行扫描,以对并发标记阶段遭到破坏的对象引用关系进行修复,以保证执行清理之前对象引用关系是正确的。这一阶段需要STW,时间也比较短暂
并发清理(concurrent-sweeping):清理垃圾对象,这个过程与用户线程并发执行,不停顿
并发重置(reset):重置CMS收集器的数据结构,等待下一次GC
可以看到,整个过程中需要STW的阶段仅有初始标记和*重新标记阶段,所以可以说它的停顿时间比较短(当然吞吐量可能会受影响)。
CMS的缺陷
由于CMS是基于标记-清理算法的,因此会产生大量的内存碎片。这很可能会出现老年代虽然有大量不连续的空闲内存,但很难找到连续的内存空间来给对象分配,不得不提前触发一次Full GC的情况。
针对这一点,CMS提供了一个-XX:+UseCMSCompactAtFullCollection开关(默认开启),用于在CMS要gg的时候进行内存碎片整理从而得到连续的内存空间。这样内存碎片的问题可以解决,但STW的时间也相应变长。
另外,CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
注意并发(Concurrent)和并行(Parallel)的区别:
并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行);
并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;
所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并发的.
5. G1收集器Garbage First Collector
G1(Garbage First)收集器是HotSpot JVM最新的垃圾收集器,它最大的特点就是将堆内存划分成多个连续的区域(region),每个区域大小相等。因此在G1中新生代与老年代都是由若干个Region组成(不需要连续)。Region的大小是可以重新设置的。
G1的优点:可以非常精确地控制停顿;老年代采用标记-压缩算法,避免了内存碎片的问题。
G1收集器的运作大致可划分为以下几个步骤:
初始标记:初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记:并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记:最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1会在内部维护一个优先列表,通过一个合理的模型,计算出每个Region的收集成本和收益期望并量化,这样每次进行GC时,G1总是会选择最适合的Region(通常垃圾比较多)进行回收,使GC时间满足设置的条件。
Java 8和G1收集器
G1收集器在Java 8 u20上最漂亮的优化是String去重(String Deduplication)。String对象和它内部使用的char[]数组会占用比较多的内存,因此优化过的G1收集器会把重复的String对象指向同一个char[]数组,避免多个副本存在在堆里。可以使用-XX:+UseStringDeduplication参数来打开这一功能。
参考
网友评论