一.概述
收集算法(JVM之垃圾回收-垃圾收集算法)是内存回收的抽象策略,垃圾收集器就是内存回收的具体实现。
JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。
就像没有最好的算法一样,垃圾收集器也没有最好,只有最合适。我们能做的就是根据具体的应用场景选择最合适的垃圾收集器。
二.Serial收集器
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了(新生代采用复制算法,老生代采用标志整理算法)。
这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,
更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" :将用户正常工作的线程全部暂停掉),直到它收集结束。
看图理解:
(1)新生代采用复制算法,Stop-The-World
(2)老年代采用标记-整理算法,Stop-The-World
当它进行GC工作的时候,虽然会造成Stop-The-World,正如每种算法都有存在的原因,该串行收集器也有存在的原因:
因为简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率。
所以Serial收集器对于运行在client模式下的应用是一个很好的选择(到目前为止,它依然是虚拟机运行在client模式下的默认新生代收集器)
串行收集器的缺点很明显,虚拟机的开发者当然也是知道这个缺点的,所以一直都在缩减Stop The World的时间。
在后续的垃圾收集器设计中停顿时间在不断缩短(但是仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)
1、特点
(1)针对新生代的收集器;
(2)采用复制算法;
(3)单线程收集;
(4)进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
2、应用场景
(1)依然是HotSpot在Client模式下默认的新生代收集器;
(2)也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);
(3)对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
(4)在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的
3、设置参数
添加该参数来显式的使用串行垃圾收集器:
"-XX:+UseSerialGC"
二.ParNew收集器(Serial收集器的多线程版本-使用多条线程进行GC)
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
CMS收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。
收集器的运行过程如下图所示:
1、特点
(1)除了多线程外,其余的行为、特点和Serial收集器一样;
(2)如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;
(3)Serial收集器共用了不少代码;
2、应用场景
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
3、设置参数
指定使用CMS后,会默认使用ParNew作为新生代收集: "-XX:+UseConcMarkSweepGC" 强制指定使用ParNew: "-XX:+UseParNewGC" 指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相: "-XX:ParallelGCThreads"
4、为什么只有ParNew能与CMS收集器配合
(1)CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
(2)CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作
(3)因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
三.Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器关注点是吞吐量(如何高效率的利用CPU)。
CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
(吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)。
比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。)
运行示意图:
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,不进行手工优化,可以选择把内存管理优化交给虚拟机去完成。
1、特点
(1)新生代收集器;
(2)采用复制算法;
(3)多线程收集;
(4)CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
2、应用场景
(1)高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
(2)当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
(3)例如,那些执行批量处理、订单处理(对账等)、工资支付、科学计算的应用程序;
3、设置参数
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:
控制最大垃圾收集停顿时间:
"-XX:MaxGCPauseMillis"
(1)控制最大垃圾收集停顿时间,大于0的毫秒数;
(2)MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;
4、设置垃圾收集时间占总时间的比率
"-XX:GCTimeRatio"
设置垃圾收集时间占总时间的比率,0 < n < 100的整数;
GCTimeRatio相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是: 1 / (1 + n) 。
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5% = 1/(1+19);默认值是1% = 1/(1+99),即n=99;
垃圾收集所花费的时间是年轻一代和老年代收集的总时间;
如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;
5、GC自适应的调节策略(GC Ergonomics)
另外还有一个参数:
"-XX:+UseAdptiveSizePolicy"
开启这个参数后,就不用手工指定一些细节参数,如:
(1)新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
(2)JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics);
(3)这是一种值得推荐的方式:
a.只需设置好内存数据大小(如"-Xmx"设置最大堆);
b.然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;
c.那些具体细节参数的调节就由JVM自适应完成;
这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;
四.Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。
它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
1、特点
(1)针对老年代;
(2)采用"标记-整理-压缩"算法(Mark-Sweep-Compact);
(3)单线程收集;
Serial/Serial Old收集器运行示意图在前面有。
2、应用场景
(1)主要用于Client模式;
(2) 而在Server模式有两大用途:
a.在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配Parallel Scavenge收集器);
b.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;
五.Parallel Old收集器
Parallel Scavenge收集器的老年代版本。
使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
在JDK1.6才有的。
1、特点
(1)针对老年代;
(2)采用"标记-整理-压缩"算法;
(3)多线程收集;
Parallel Scavenge/Parallel Old收集器运行示意图如下:
2、应用场景
(1)JDK1.6及之后用来代替老年代的Serial Old收集器;
(2)特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge(新生代)加Parallel Old(老年代)收集器的"给力"应用组合;
3、设置参数
指定使用Parallel Old收集器:
"-XX:+UseParallelOldGC"
六.CMS(Concurrent Mark Sweep)收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常适合在注重用户体验的应用上使用。
1、特点
(1)针对老年代;
(2)基于"标记-清除"算法(不进行压缩操作,会产生内存碎片);
(3)以获取最短回收停顿时间为目标;
(4)并发收集、低停顿;
(5) 需要更多的内存;
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
2、应用场景
(1)与用户交互较多的场景;(如常见WEB、B/S-浏览器/服务器模式系统的服务器上的应用)
(1)希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;
3、CMS收集器运作过程
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程可分为四个步骤:
(1)初始标记: 暂停所有的其他线程,初始标记仅仅标记GC Roots能直接关联到的对象,速度很快;
(2)并发标记
a.并发标记就是进行GC Roots Tracing的过程;
b.同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。
因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方;
(3)重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用多线程并行执行来提升效率);
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
(4)并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象;
由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。
所以总体来说,CMS的内存回收是与用户线程一起“并发”执行的。
CMS收集器运行示意图如下:
4、设置参数
指定使用CMS收集器
"-XX:+UseConcMarkSweepGC"
5、CMS&Parallel Old
总体来看,CMS与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
(原因:CMS不进行内存空间整理节省了时间,但是可用空间不再是连续的了,垃圾收集也不能简单的使用指针指向下一次可用来为对象分配内存的地址了。
相反,这种情况下,需要使用可用空间列表。即,会创建一个指向未分配区域的列表,每次为对象分配内存时,会从列表中找到一个合适大小的内存区域来为新对象分配内存。
这样做的结果是,老年代上的内存的分配比简单实用碰撞指针分配内存消耗大。这也会增加年轻代垃圾收集的额外负担,因为老年代中的大部分对象是在新生代垃圾收集的时候从新生代提升为老年代的。)
当新生代对象无法分配过大对象,就会放到老年代进行分配。