目录
有关GC算法讲解见:【JVM笔记】GC算法详解
java虚拟机规范对垃圾收集器应该如何实现没有任何规定,因为没有所谓最好的垃圾收集器出现,更不会有万金油垃圾收集器,只能是根据具体的应用场景选择合适的垃圾收集器。JVM虚拟机中有很多垃圾收集器,在绝大多数情况下JVM会自动选择合适的收集器进行垃圾回收。
每个收集器都有自己适合的分代,收集器之间也大多是成对配合使用的,如下图所示。
一、Serial收集器
Serial收集器(串行收集器)是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ,简称STW),直到它收集结束。它在单核CPU时代被广泛应用。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集对于运行在Client模式下的虚拟机来说是个不错的选择。
二、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。ParNew收集器在执行的时候会有多条垃圾收集线程并行工作,但也是会暂停其他所有进程(STW)。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
三、Parallel Scavenge收集器
Parallel Scavenge 收集器类(并行收集器)似于ParNew 收集器。它是jdk1.8的默认是默认收集器。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在困难的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
Parallel Scavenge收集器执行的时候多条垃圾收集线程并行工作,在多核CPU下效率更高,应用线程仍然处于等待状态(STW),但是因为他是多个GC线程并行执行垃圾回收,所以垃圾回收的比较快,应用线程等待的时间比Serial收集器少很多。
四、Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
五、Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
六、CMS收集器
并行和并发概念补充:
- 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并行指两个或多个事件在同一时刻发生。并行一般就需要多CPU支持。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行)。并发是指两个或多个事件在同一时间间隔内发生。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记(CMS initial mark): 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快。多线程标记。
- 并发标记(CMS concurrent mark): 同时开启并发标记线程和用户线程,用一个闭包结构去从GC Root开始对堆中对象进行可达性分析,找出存活的对象可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记(CMS remark): 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除(CMS concurrent sweep): 开启用户线程,同时GC线程开始对为标记的区域做清扫。在这个期间就会产生浮动垃圾,就是在并发清理期间用户线程执行期间还是有可能产生垃圾,这些垃圾在本次GC中是不能被回收的,这些垃圾就是浮动垃圾。浮动垃圾只能等到下次GC被清除。
- 并发重置:准备进行下一次GC
CMS收集器开启后,年轻代使用STW式的并行收集(ParNew收集器),老年代回收采用CMS进行垃圾回收,对延迟的关注也主要体现在老年代CMS上。
CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对CPU资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算***导致收集结束时会有大量空间碎片产生。
为什么除了Serial收集器外只有ParNew能与CMS收集器配合?
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的传统框架代码,所以除了Serial收集器外,只有ParNew能与CMS收集器配合。
七、G1收集器
之前介绍的几组垃圾收集器组合,都有几个共同点:
- 年轻代、老年代是独立且连续的内存块;
- 年轻代收集使用单eden、双survivor进行复制算法;
- 老年代收集必须扫描整个老年代区域;
- 都是以尽可能少而快地执行GC为设计原则。
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1收集器在jdk1.9后成为了JVM的默认垃圾收集器。
G1收集器放弃了之前的收集器中所使用的分代思想,引入分区(Region)的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。所以在G1中上面那个图已经不适用了,而是要使用下面这个图。
分区和卡片的关系
- 分区 Region
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
Humongous区是大对象存放的区域,在之前的收集器都有分匹配担保机制,在新生代的大对象如果存放不了就会被装入老年代,但是G1收集器中大对象会被直接放到Humongous区。G1内部做了一个优化,一旦发现没有引用指向大对象,则可直接在年轻代收集周期中被回收。
- 卡片 Card
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理。
G1收集器被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:
- 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)",即先尽可能地标注可回收的对象,等到最后再根据用户设定的等待停顿时间进行筛选回收。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,它会日常进行垃圾收集,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆(既能回收新生代,又能回收老年代),但是还是保留了分代的概念。G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
- 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集Young GC),也可能在收集年轻代的同时,包含部分老年代分区(混合收集 Mixed GC),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。G1收集器只有Young GC和 Mixed GC。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。就是在筛选回收的过程中,对要进行回收的对象进行一次筛选,G1收集器会有一张表存有所有可回收的对象,这张表会按照对象引用类型来进行回收优先级的排序,如果用户设定好停顿时间是10毫秒,假如收集器1毫秒只能回收1000个垃圾对象,那么G1收集器就会将表中回收优先级前10000个对象回收掉,剩下的可回收对象先把回收。通过这个机制就实现了可预测停顿,能让使用者设定停顿时间。所以G1收集器很适合对用户等待时长体验要求很高的系统,可以自己设定合适的等待停顿时长。
还有一个问题:
G1把内存“化整为零”(将内存区域划分成一个个的分区)的思路,以一个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。
- 这里就要引入已记忆集合Remember Set (RSet)的概念:
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过Card Table 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致分为以下几个步骤:
- 初始标记(Initial Marking):与CMS一样,该阶段仅仅只是标记一下GCRoots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,初始标记需要将Mutator线程(Java应用线程)暂停掉,也就是需要一个STW的时间段,但耗时很短。初始标记是并发执行,直到所有的分区处理完就结束标记。
- 并发标记(Concurrent Marking):并发标记线程在并发标记阶段启动,从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,不需要停顿时间。
- 最终标记(Final Marking):重新标记(Remark)是最后一个标记阶段。在该阶段中,G1需要一个暂停的时间,为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这个阶段也是并行执行的。
- 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率,所以它也需要STW的时间段,并且是并行执行。
它的过程和CMS很像:
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
总结:
G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过“首先收集尽可能多的垃圾(Garbage First)”的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。
八、怎么选择垃圾收集器?
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100m,使用串行收集器
- 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
官方推荐G1,性能高。HotSpot在JVM上力推的垃圾收集器,并赋予G1取代CMS的使命