目录
一、Serial收集器
二、ParNew收集器
三、Parallel Scavenge收集器
四、Serial Old收集器
五、Parallel Old收集器
六、CMS收集器
为什么除了Serial收集器外只有ParNew能与CMS收集器配合?
七、G1收集器
分区和卡片的关系
G1收集器的特点
G1收集器的执行步骤:
八、怎么选择垃圾收集器?
有关GC算法讲解见:【JVM笔记】GC算法详解
java虚拟机规范对垃圾收集器应该如何实现没有任何规定,因为没有所谓最好的垃圾收集器出现,更不会有万金油垃圾收集器,只能是根据具体的应用场景选择合适的垃圾收集器。JVM虚拟机中有很多垃圾收集器,在绝大多数情况下JVM会自动选择合适的收集器进行垃圾回收。
每个收集器都有自己适合的分代,收集器之间也大多是成对配合使用的,如下图所示。
Serial收集器(串行收集器)是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ,简称STW),直到它收集结束。它在单核CPU时代被广泛应用。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。ParNew收集器在执行的时候会有多条垃圾收集线程并行工作,但也是会暂停其他所有进程(STW)。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
Parallel Scavenge 收集器类(并行收集器)似于ParNew 收集器。它是jdk1.8的默认是默认收集器。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在困难的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代(需要其他收集器配合)采用标记-整理算法。
Parallel Scavenge收集器执行的时候多条垃圾收集线程并行工作,在多核CPU下效率更高,应用线程仍然处于等待状态(STW),但是因为他是多个GC线程并行执行垃圾回收,所以垃圾回收的比较快,应用线程等待的时间比Serial收集器少很多。
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
并行和并发概念补充:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
CMS收集器开启后,年轻代使用STW式的并行收集(ParNew收集器),老年代回收采用CMS进行垃圾回收,对延迟的关注也主要体现在老年代CMS上。
CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的传统框架代码,所以除了Serial收集器外,只有ParNew能与CMS收集器配合。
之前介绍的几组垃圾收集器组合,都有几个共同点:
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1收集器在jdk1.9后成为了JVM的默认垃圾收集器。
G1收集器放弃了之前的收集器中所使用的分代思想,引入分区(Region)的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。所以在G1中上面那个图已经不适用了,而是要使用下面这个图。
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
Humongous区是大对象存放的区域,在之前的收集器都有分匹配担保机制,在新生代的大对象如果存放不了就会被装入老年代,但是G1收集器中大对象会被直接放到Humongous区。G1内部做了一个优化,一旦发现没有引用指向大对象,则可直接在年轻代收集周期中被回收。
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理。
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即可保证不对全堆扫描也不会有遗漏。
它的过程和CMS很像:
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过“首先收集尽可能多的垃圾(Garbage First)”的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。
官方推荐G1,性能高。HotSpot在JVM上力推的垃圾收集器,并赋予G1取代CMS的使命