G1垃圾回收器
1、背景
G1(Garbage First Collector 垃圾优先的收集器),说是一种全新的,其实G1垃圾收集器已经出现了N多年了,只是从发展到成熟是需要经历一定的过程,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS, 可见G1肯定有它独特的地方,它跟我们之前所学的各种垃圾底层是完全不一样的,比如最明显的不同是以前的分代收集方式是将堆划分为新生代、老年代两个区域,而新生代又划分为Eden和两个Survivor,也就是从物理的结构就明确的做了区域划分,但是!!!G1它依据的物理形态跟我们之前所接触的垃圾收集器完全不一样了,也就是明显会感觉到G1里面的堆内存没有明显的区域划分
2、吞吐量:
-
吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。
-
如下方式来衡量一个系统吞吐量的好坏:
1、在一个小时内同一个事务(或者任务、请求)完成的次数(tps,实际中还会经常见qps,每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准)。
2、数据库一小时可以完成多少次查询。 -
对于关注吞吐量的系统,卡顿是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单次快速的响应并不值得考虑。
3、响应能力:
- 响应能力指一个程序或者系统对请求是否能够及时响应,比如:
1、一个桌面UI能多快地响应一个事件。
2、一个网站能够多快返回一个页面请求。
3、数据库能够多快返回查询的数据。 - 对于这类对响应能力敏感的场景,长时间的停顿是无法接受的。
以上是用来评价一个系统的两个很重要的指标,介绍这两个指标的原因是因为G1就是用来解决这样的问题而应运而生的。
4、G1垃圾回收器
- g1收集器是一个面向服务端的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。
- 它满足短时间gc停顿的同时达到一个较高的吞吐量。
- JDK7以上版本适用【通过配置JVM的参数来指定既可】。
以上可以看到G1在吞吐量和响应能力上都进行了兼顾。
5、G1收集器的设计目标:
(延迟可控的情况下获得尽可能高的吞吐量)
- 与应用线程同时工作,几乎【注意措辞】不需要stop the world(与CMS类似);
- 整理剩余空间,不产生内存碎片(CMS只能在Full GC时,用stop the world整理内存碎片)。
- GC停顿更加可控;【要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。同时对于CMS来说如果出现了Full GC时,则会对新生代和老年代的堆内存进行完整的整理,停顿时间就不可控了】
- 不牺牲系统的吞吐量;
- gc不要求额外的内存空间(CMS需要预留空间存储浮动垃圾【这个在学习CMS中已经阐述过了,其实就是CMS回收的过程跟用户线程是并发进行的,所在在标记或者清除的同时对象的引用还会被改变,使得原来对象本来不是垃圾,当CMS清理时该对象已经变成了垃圾了,但是CMS认为它还不是垃圾,所以该对象的清除工作就会放到下一次了,所以将这种对象则称之为浮动垃圾】)
6、G1内存模型
1、G1堆结构
image.png image.png(1)分区 Region
G1 采用了分区(Region)的思路,堆被划分为一个个相等的不连续的内存区域(regions),每个regions都有一个分代的角色:eden、survivor、old。对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化。每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
(2)卡片 Card
在每个分区内部又被分成了若干个大小为512 Byte 卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见 RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为“0”,既标记为被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,key是当前Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
(3)已记忆集合 Remember Set(RSet)
在串行和并行收集器中,GC 通过整堆扫描,来确定对象是否处于可达路径中。然而 G1 为了避免 STW 式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的 RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在 RSet 中,引用源自本分区的对象,当然不用落入 RSet 中;同时,G1 GC 每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在 RSet 中记录。最后只有老年代的分区可能会有 RSet 记录,这些分区称为拥有 RSet 分区(an RSet’s owning region)。
(4)Humongous区域
在G1中,还有一种特殊的区域,叫Humongous区域。如果一个对象占用的空间达到或者超过了分区容量的50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分 Humongous区域,它专门用来存放巨型对象。如果一个H区域装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC
2、收集集合(CSet)
image.png收集集合(CSet)代表每次 GC 暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。
①:年轻代收集 CSet 只容纳年轻代分区,
②:混合收集会通过某种算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中。
该算法是:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
年轻代收集集合 CSet of Young Collection(详细版)
应用线程不断活动后,年轻代空间会被逐渐填满。当 JVM 分配对象到 Eden 区域失败(Eden 区已满)时,便会触发一次 STW 式的年轻代收集。在年轻代收集中,Eden 分区存活的对象将被拷贝到 Survivor 分区;原有 Survivor 分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到 PLAB 中,新的 survivor 分区和老年代分区。而原有的年轻代分区将被整体回收掉。
同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到 Survivor 分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor 尺寸、Survivor 填充容量 -XX:TargetSurvivorRatio(默认50%)、最大任期阈值 -XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。
混合收集集合 CSet of Mixed Collection
年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比 IHOP 阈值 -XX:InitiatingHeapOccupancyPercent(默认45%)时,G1 就会启动一次混合垃圾收集周期。为了满足暂停目标,G1 可能不能一口气将所有的候选分区收集掉,因此 G1 可能会产生连续多次的混合收集与应用线程交替执行,每次 STW 的混合收集与年轻代收集过程相类似。
3、可预测的停顿模型
G1使用了gc停顿可预测的模型,来满足用户设定的gc停顿时间,根据用户设定的目标时间,G1会自动地选择哪些region要清除,一次清除多少个region。
这是G1相对于CMS的另一个大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
- 由于分区的原因,G1可以只选部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMS GC,G1未能做到CMS在最好情况下的延时停顿,但是最长情况要好很多
- 停顿时间的设置并不是越短越好,设置的时间越短意味着每次收集的Cset越小,导致垃圾逐步积累变多,最终不得不退化成Full GC(Serial GC),停顿时间设置过长,那么会导致每次都会产生长时间的停顿个,影响了程序对外的响应时间
7、G1垃圾回收器的缺点
1、相对于CMS,G1还不具备全方位、压倒性的优势,比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高
2、从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用则发挥其优势,平衡点6 ~ 8GB之间
8、G1垃圾回收过程
1、年轻代GC(Young GC)
年轻代垃圾回收只会回收Eden区和Survivor区。年轻代也使用了分区机制主要是因为便于代大小的调整
年轻代回收时,首先G1停止应用程序的执行(Stop - The - World),G1创建回收集(CSet),对于YGC来说,整个年轻代(Eden区 + Survivor区)都是CSet
然后开始如下回收过程(并行操作,多个收集器的线程同时工作,但是用户线程处于等待状态。)
阶段1:根扫描
GC Roots包括
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
阶段2:更新RSet
处理dirty card队列更新RS(更新完后,RSet可以准确的反映老年代对所在的内存分段中对象的引用)
对于应用程序的引用赋值语句Object.field = object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对DIrty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系
那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多
阶段3:处理RSet
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为存活的对象
阶段4:复制对象
拷贝存活的对象到survivor/old区域。① Survivor区内存中存活的对象如果年龄未达到阈值,年龄会加1,达到阈值会被赋值到Old区。 ② 如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间
阶段5:处理引用队列
软引用、弱引用、虚引用处理
2、并发标记过程(Concurrent Marking)
1、初始标记(inital mark,STW):它标记了从GC Root开始直接可达的对象,并且会触发一次年轻代的GC
2、根区域扫描(Root Region Sacnning):触发的年轻代GC完成后,所有新复制到 Survivor 分区(根区域)的对象,都需要被扫描并标记成根,G1 GC扫描Survivor取直接可达的老年代区域对象,并标记被引用的对象。这个过程必须在young GC之前完成
3、并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
4、重新标记(Remark,STW):由于应用程序持续进行,需要修正上一次的标记结果,SATB算法
5、独占清理(Exclusive Cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为混合回收做铺垫,是STW的。这个阶段不会实际上做垃圾的收集
6、并发清理(Concurrent Cleanup):清除空Region(没有存活对象的),加入到free list。
3、混合回收(Mixed GC)
在并发标记过程后进行拷贝存活对象