垃圾收集器也有类似CAP理论的矛盾,具体如下面三个考量指标:
上面三个考量指标无法同时全部满足最优,只能满足其中的两个,而牺牲其中一个的部分效率。
吞吐量关注的是,在一个指定的时间范围内,最大化一个应用的工作量。
如下方式来衡量一个系统吞吐量的好坏:
对于关注吞吐量的系统,偶尔卡顿是可以接受的,因为这个系统关注的是长时间大量任务的执行能力,单次快速的响应并不值得考虑。
响应能力是一个应用或者一个系统是否能够及时快速的响应,比如:
对于这类响应能力敏感,追求低延迟的场景,长时间的卡顿是不能忍受的。
为了加快内存扫描的数据,GC垃圾收集器通常会在内存中使用一些数据结构如卡表、记忆集等来存储对象直接的引用,而这些数据本身是需要占用堆的内存空间的。记录的信息越多,扫描时就会越快,同时也越占内存。
随着硬件的成本越来越低,机器的内存也越来越大,GC收集器占用的内存基本上可以容忍,而吞吐量可以通过集群(增加机器)来解决,所以STW的时间成为JVM急迫解决的问题,如果还是按照传统的分代模型,使用传统的垃圾收集器,那么STW的时间将会越来越长。
在传统的垃圾收集器中,STW的时间是无法预测的,有没有一种办法,能够首先定义一个停顿时间,然后反向推算收集的内容呢?就像是领导在年初制定KPI一样,分配的任务多就多干些,分配的任务少就少干点。
G1的思路说起来也类似,它不要求每次都把垃圾清理的干干净净,它只是努力做它认为对的事情。
我们要求G1,在任意1秒的时间内,停顿不得超过10ms,这就是在给它制定KPI。G1会尽量达成这个目标,它能够反向推算出本次要收集的大体区域,以增量的方式完成收集。
这也是使用G1垃圾回收器(-XX:+UseG1GC
)不得不设置的一个参数:-XX:MaxGCPauseMillis=10
,这个参数默认为200ms。
G1的适用场景:
为了实现STW的时间可预测,首先要有一个思想上的改变,使用分而治之。G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region可能是Eden,也有可能是Survivor,也有可能是Old,另外Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象(大对象直接进入老年代)。每个Region的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待。
G1在逻辑上还是划分Eden(所有Eden region之和)、Survivor(所有Survivor region之和)、OLd(所有Old region之和),但是物理上他们不是连续的,而且同一块region在不同时期的角色可以是不一样的,有可能现在这块region存放的是Eden对象,进行一个YGC之后,下一次分配在上面的对象可能是Old对象。
收集集合(CSet,CollectionSet):一组可被回收的Region的集合,在CSet中存活的对象会在GC的过程中被复制到另一个region中,CSet中的Region可以来自Eden、Survivor、Eden。
卡表是为了解决跨代引用而诞生的,假设在CMS垃圾收集器中,要回收新生代的对象,那么就必须扫描整个老年代,而引入卡表,就只需要扫描卡表中Dirty区域即可,这样避免了扫描整个老年代,大大加快了并发标记的速度。
卡表(Card Table)底层是使用Bitmap实现的,在G1中,大小为1M的Region按512Byte可以划分为2048(1M/512Byte)个卡页(Card Page),Bitmap中的一个bit代表一个Region中的一页,这样1M的Region总共需要2048个bit位来表示,默认bit位为0,表示这一页不引用任何其他Region中的对象,当这一页中引用了其他Region中的对象时,将bit位置为Dirty(1)。
假设现在Region A中的对象第4页引用了Region B中的对象,那么将Region A的卡表的第3(数组索引从0开始)个bit为置为Dirty(1),这就是所谓的point-out
。
在CMS中,可以将老年代的所有空间使用一个卡表来标记哪些老年代对象所在的页引用了新生代的对象,这样在执行YGC时,只需要扫描卡表中标记为Dirty的区域上的对象即可,而无需扫描整个堆。
而在G1中,单靠卡表无法解决跨Region引用问题,因为G1中划分为多个Region,存在多对多(一个Region中对象引用多个Region中的对象,一个Region中的对象被多个Region引用)的关系,不像CMS中的一对一(老年代对新生代),所以还需要引入下面的数据结构-记忆集。
在G1中,每个Region都有一个记忆集(RememberedSet)的数据结构,用来记录其他Region中对象到当前Region中对象的引用关系(point-in
)。RSet底层使用HashTable实现,key为引用对象所在Region的内存起始地址,value为引用对象所在Card Table的index。
同样假设现在Region A中的对象第4块引用了Region B中的对象,那么将Region A的卡表的第3个bit为置为Dirty(1),然后将Region B中的RSet中增加一条记录,其中key为Region A的内存起始地址,value为Region A的对象所在卡表中的索引3,这样在回收Region B时,只需要将Region B的RSet作为GC ROOTS对象扫描即可。
RSet的更新并不是同步完成的,G1会把所有的引用关系都先放入到一个队列中,称为Dirty Card Queue(DCQ),然后使用单独的线程消费这个队列来完成更新,这么做是因为对象的引用变更太频繁,使用队列进行异步削峰,可以使用参数-XX:G1ConcRefinementThreads
这个参数来指定消费线程的数目。
使用空间来换时间,用额外的空间来维护引用信息,通常需要消耗5%~10%的空间。
写屏障(write barrier):当对象的引用发生变化时,插入一个写屏障来维护RSet。这个写屏障与Java内存模型中的写屏障不是一个概念。
G1的垃圾回收过程可能会出现下面三种模式:
YGC还是一样,在新生代满了之后触发YGC,新生代的垃圾回收方式还是采用复制算法,将存活的对象复制到Survivor或者老年代。
YGC过程:
MixedGC的触发时机:达到-XX:InitiatingHeapOccupanyPercent
阈值开始并发标记,默认值为45,也就是当已经分配的内存加上即将分配的内存超过堆内存总容量的45%时就会开始并发标记。
MixedGC分为两步:
全局并发标记又分为以下四个步骤:
TAMS是什么?要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。
当晋升失败、疏散失败、大对象分配失败、Evac失败时,有可能触发Full GC,在JDK10之前,Full GC是串行的,JDK10之后引入了并行Full GC。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://morris131.blog.csdn.net/article/details/113822908
内容来源于网络,如有侵权,请联系作者删除!