如果说前面介绍的收集算法(JVM之垃圾回收-垃圾收集算法)是内存回收的抽象策略,那么垃圾收集器就是内存回收的具体实现。
JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。
就像没有最好的算法一样,垃圾收集器也没有最好,只有最合适。我们能做的就是根据具体的应用场景选择最合适的垃圾收集器。
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了(新生代采用复制算法,老生代采用标志整理算法)。大家看名字就知道这个收集器是一个单线程收集器了。
它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” :将用户正常工作的线程全部暂停掉),直到它收集结束。
看图理解:
上图中:
当它进行GC工作的时候,虽然会造成Stop-The-World,正如每种算法都有存在的原因,该串行收集器也有存在的原因:因为简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率。所以Serial收集器对于运行在client模式下的应用是一个很好的选择(到目前为止,它依然是虚拟机运行在client模式下的默认新生代收集器)
串行收集器的缺点很明显,虚拟机的开发者当然也是知道这个缺点的,所以一直都在缩减Stop The World的时间。
在后续的垃圾收集器设计中停顿时间在不断缩短(但是仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)
整理一下前面关于Serial收集器的知识
添加该参数来显式的使用串行垃圾收集器:
"-XX:+UseSerialGC"
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
CMS收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。
收集器的运行过程如下图所示:
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
指定使用CMS后,会默认使用ParNew作为新生代收集:
"-XX:+UseConcMarkSweepGC"
强制指定使用ParNew:
"-XX:+UseParNewGC"
指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相:
"-XX:ParallelGCThreads"
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器关注点是吞吐量(如何高效率的利用CPU)。
CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。(吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)。比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。)
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,不进行手工优化,可以选择把内存管理优化交给虚拟机去完成。
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:
"-XX:MaxGCPauseMillis"
"-XX:GCTimeRatio"
设置垃圾收集时间占总时间的比率,0 < n < 100的整数;
GCTimeRatio相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是: 1 / (1 + n) 。
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5% = 1/(1+19);默认值是1% = 1/(1+99),即n=99;
垃圾收集所花费的时间是年轻一代和老年代收集的总时间;
如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;
另外还有一个参数:
"-XX:+UseAdptiveSizePolicy"
开启这个参数后,就不用手工指定一些细节参数,如:
Serial收集器的老年代版本,它同样是一个单线程收集器。
它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案
Parallel Scavenge收集器的老年代版本。
使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
在JDK1.6才有的。
指定使用Parallel Old收集器:
"-XX:+UseParallelOldGC"
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常适合在注重用户体验的应用上使用。
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程可分为四个步骤:
由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。
所以总体来说,CMS的内存回收是与用户线程一起“并发”执行的。
指定使用CMS收集器
"-XX:+UseConcMarkSweepGC"
面向并发设计的程序都对CPU资源比较敏感(并发程序的特点)。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。(在对账系统中,不适合使用CMS收集器)。
CMS的默认收集线程数量是=(CPU数量+3)/4; 当CPU数量越多,回收的线程占用CPU就少。
也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。(比如 CPU=2时,那么就启动一个线程回收,占了50%的CPU资源。)
(一个回收线程会在回收期间一直占用CPU资源)
针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);
类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;
但效果并不理想,JDK1.6后就官方不再提倡用户使用。
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;
这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;
也可以认为CMS所需要的空间比其他垃圾收集器大; 可以使用"-XX:CMSInitiatingOccupancyFraction",设置CMS预留老年代内存空间; (详解见名词解释)
由于CMS是基于“标记+清除”算法来回收老年代对象的,因此长时间运行后会产生大量的空间碎片问题,可能导致新生代对象晋升到老生代失败。
由于碎片过多,将会给大对象的分配带来麻烦。因此会出现这样的情况,老年代还有很多剩余的空间,但是找不到连续的空间来分配当前对象,这样不得不提前触发一次Full GC。
使用"-XX:+UseCMSCompactAtFullCollection"和"-XX:+CMSFullGCsBeforeCompaction",需要结合使用。
"-XX:+UseCMSCompactAtFullCollection"
为了解决空间碎片问题,CMS收集器提供−XX:+UseCMSCompactAlFullCollection标志,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
由于合并整理是无法并发执行的,空间碎片问题没有了,但是有导致了连续的停顿。因此,可以使用另一个参数−XX:CMSFullGCsBeforeCompaction,表示在多少次不压缩的Full GC之后,对空间碎片进行压缩整理。
可以减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
总体来看,CMS与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
(原因:CMS不进行内存空间整理节省了时间,但是可用空间不再是连续的了,垃圾收集也不能简单的使用指针指向下一次可用来为对象分配内存的地址了。相反,这种情况下,需要使用可用空间列表。即,会创建一个指向未分配区域的列表,每次为对象分配内存时,会从列表中找到一个合适大小的内存区域来为新对象分配内存。这样做的结果是,老年代上的内存的分配比简单实用碰撞指针分配内存消耗大。这也会增加年轻代垃圾收集的额外负担,因为老年代中的大部分对象是在新生代垃圾收集的时候从新生代提升为老年代的。)
当新生代对象无法分配过大对象,就会放到老年代进行分配。
上一代的垃圾收集器(串行serial, 并行parallel, 以及CMS)都把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation)。
注:堆内存中都可以认为是Java对象。
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。
G1的使命是在未来替换CMS,并且在JDK1.9已经成为默认的收集器。
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。在低停顿的同时实现高吞吐量。
这就保证了在有限的时间内可以获取尽可能高的收集效率;
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
在其他的分代收集器,也存在这样的问题(而G1更突出):回收新生代也不得不同时扫描老年代?
这样的话会降低Minor GC的效率;
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;
就可以保证不进行全局扫描,也不会有遗漏。
具体什么情况下应用G1垃圾收集器比CMS好,可以参考以下几点(但不是绝对):
建议:
可以通过下面的参数,来设置一些G1相关的配置。
指定使用G1收集器:
"-XX:+UseG1GC"
当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45:
"-XX:InitiatingHeapOccupancyPercent"
为G1设置暂停时间目标,默认值为200毫秒:
"-XX:MaxGCPauseMillis"
设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region:
"-XX:G1HeapRegionSize"
新生代最小值,默认值5%:
"-XX:G1NewSizePercent"
新生代最大值,默认值60%:
"-XX:G1MaxNewSizePercent"
设置STW期间,并行GC线程数:
"-XX:ParallelGCThreads"
设置并发标记阶段,并行执行的线程数:
"-XX:ConcGCThreads"
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要"Stop The World",但速度很快;
从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行
并不能保证可以标记出所有的存活对象;(在分析过程中会产生新的存活对象)
修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
G1采用多线程并行执行来提升效率;且采用了比CMS更快的初始快照算法:Snapshot-At-The-Beginning (SATB)。
首先排序各个Region的回收价值和成本;
然后根据用户期望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
可以并发进行,降低停顿时间,并增加吞吐量;
G1在标记过程中,每个区域的对象活性都被计算,在回收时候,就可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Remark(重新标记)阶段新算法的运用,以及收集过程中的压缩,都弥补了CMS不足。
引用Oracle官网的一句话:“G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS)”。
G1计划作为并发标记-清除收集器(CMS)的长期替代品