G1(Garbage-First)垃圾回收器是在jdk7版本开始被引进的,它的特性在于可以尽量的知足用户对停顿时间的要求同时还保持较高的吞吐。G1的定位是取代CMS,相比CMS,G1可以更有效的避免碎片化,同时可让用户指定预期的停顿时间。算法
G1一样是分代的垃圾回收,可是不一样的是G1把整个堆分红了大小相等的块(称为region),每一个region能够被分配为不一样的角色(young、eden、old等等),这意味着不一样代的内存大小是不固定的,能够灵活地调整。数据结构
G1会在jvm启动时肯定region size(最小1M,最大32M),一般G1会尽可能把堆分为2048个相同大小的region,具体的region size由此计算,也能够在jvm参数里显示指定。不一样的region会被分配到不一样的逻辑角色,好比eden/old,同一个逻辑角色的不一样region也不必定是连续的。并发
G1的运行机制与CMS相似,G1会进行并发的全局标记来判断对象的存活与否,在标记结束后,G1就能得知哪些region中的垃圾最多,而后就先回收这部分region,这就是G1的名字的由来。G1采用了一个停顿时间预测的模型来尽量知足用户指定的停顿时间,根据用户指定的停顿时间来选择要回收哪些region。jvm
G1在进行垃圾回收时采用的是复制算法,G1会把各个region中残留的存活对象复制到单独的region中,这样在回收过程当中就完成了内存的整理。为了下降复制过程当中停顿的时间,整个复制过程是并行的,而CMS并不会进行内存整理,ParallelOld则是会直接整理整个堆,显然会明显增长停顿时间。oop
发生young gc时,存活的对象被复制到survivor区,若是对象的年龄超过阈值,那么会把它晋升到old区。整个young gc过程当中是STW的,同时也会从新计算出下一次GC时的eden区和survivor区的大小,计算过程当中也会考虑用户指定的目标停顿时间。由于region的设计,要调整各个分区的大小实际上很是容易。post
并发标记是G1中的一个重要阶段,这个阶段包括若干个步骤,经过并发标记来收集各个region的使用状况等信息,协助达到用户指定的停顿时间。性能
这一步是和young gc一块儿顺带着执行的,首先标记出gc roots直接可达的对象,线程
young gc事后,survivor中的对象都被标记为root region,这时扫描由survivor区直接可达的old区并标记。这一阶段必须在新一轮的young gc前执行完毕。若是这时又须要young gc,那么会等待扫描完成才会进行。设计
扫描整个堆,标记存活的对象,整个阶段是与应用程序并行的,可能被young gc打断。这个阶段下会不断从扫描栈取出引用,递归地扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程当中还会扫描SATB write barrier所记录下的引用。指针
在完成并发标记后,每一个Java线程还会有一些剩下的SATB write barrier记录的引用还没有处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只须要扫描SATB buffer,而CMS的remark须要从新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(无论对象死活)都会被看成根集合的一部分,于是CMS remark有可能会很是慢。
这阶段会清理各个region,同时更新Rset,若是有空的region就把它释放掉。
把存活的对象拷贝到新的region。
在经历了一个完整的标记周期事后,G1会在下一次young gc的时刻转换成混合gc,混合gc下,G1可能会把一部分old区的region加入Cset中,利用young gc的算法清理一部分old region。当G1回收了足够多的old region,又会从新回到young gc,直到下一次并发标记周期完成。
G1的目标是要代替CMS,它把整个堆空间划分红了不一样的region来进行管理,使得分配和回收更加灵活。G1的主要活动包括young gc、mixed gc以及并发标记,它会根据用户指定的目标停顿时间来决定要对哪些内存区域进行回收。
RSet用于记录指向某个region的引用,每一个region对应一个RSet,这个数据结构里记录了哪些其余region包含了指向这个region的对象的引用。CSet记录了GC过程当中会被回收的region,CSet中存活的对象在GC过程当中都会被复制到新的空的region。Rset和Cset都是为了帮助GC而产生的额外的数据结构。
G1的heap与HotSpot VM的其它GC同样有一个覆盖整个heap的card table。逻辑上说,G1的RSet是每一个region有一份。这个RSet记录的是从别的region指向该region的card。因此这是一种“points-into”的Remembered Set。用card table实现的Remembered Set一般是points-out的,也就是说card table要记录的是从它覆盖的范围出发指向别的范围的指针。以分代式GC的card table为例,要记录old -> young的跨代指针,被标记的card是old gen范围内的。
G1则是在points-out的card table之上再加了一层结构来构成points-into RSet:每一个region会记录下到底哪些别的region有指向本身的指针,而这些指针分别在哪些card的范围内。这个RSet实际上是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。
G1在concurrent mark阶段使用了SATB算法来避免对象的漏标记,而SATB是snapshot at the beginning的缩写。简单来讲,SATB的思路就是认定在GC开始时存活的对象就是存活的,此时整个堆内的全部对象造成一个快照(snapshot);同时认定在GC过程当中新产生的对象也都是存活的,而剩下的不可达的对象则都是垃圾了。
而G1是如何肯定哪些对象是在GC开始后新产生的呢,这依赖两个指针:prevTAMS和nextTAMS。TAMS是top at mark start的缩写,这里就要再介绍一下region的几个指针了:
在每次concurrent mark开始时,将当前top赋值给nextTAMS,那么在concurrent mark过程当中,该region上新分配的对象都落在nextTAMS和top之间,G1保证这部分对象都不会被漏标,默认都是存活的。
当concurrent mark结束时,将当前的nextTAMS赋值给prevTAMS,同时根据mark的结果,将[bottom, prevTAMS]之间的对象的存活信息保存为一个bitmap,后续就能够经过这个bitmap肯定对应的对象是否存活了。
因为对象的存活标记是和应用程序并发执行的,应用程序彻底有可能在标记过程当中修改对象的引用,因此为了不漏标记,G1使用了write barrier。write barrier是指在"对引用类型字段赋值"这个动做先后的一个拦截,能够在赋值的先后进行额外的工做。在赋值前的部分的write barrier叫作pre-write barrier,在赋值后的则叫作post-write barrier,G1则使用了pre-write barrier。为了不漏标,G1会在每次引用赋值前把这个引用指向的旧的值也进行递归地标记,并默认其为存活,这样就不会漏掉任何一个snapshot中的对象了。当这个旧的值实际上再也不是存活对象时,它实际上也就成为了浮动垃圾,只能留到下一轮垃圾回收了。
能够看出,上面提到的barrier中的工做实际上都是在应用程序的线程中完成的。为了尽可能减小write barrier对性能的影响,G1将一部分本来要在barrier里作的事情挪到别的线程上并发执行。实现这种分离的方式就是经过logging形式的write barrier:应用程序只在barrier里把要作的事情的信息记(log)到一个队列里,而后另外的线程从队列里取出信息批量完成剩余的动做。
以SATB write barrier为例,每一个Java线程有一个独立的、定长的SATBMarkQueue,应用程序线程在barrier里只把old_value压入该队列中。一个队列满了以后,它就会被加到全局的SATB队列集合SATBMarkQueueSet里等待处理,而后给对应的Java线程换一个新的、干净的队列继续执行下去。
concurrent mark会按期检查全局SATB队列集合的大小。当全局集合中队列数量超过必定阈值后,concurrent marker就会处理集合里的全部队列:把队列里记录的每一个oop都标记上,并将其引用字段压到标记栈(marking stack)上等后面作进一步标记。