Appearance
内存模型-分区region
G1堆的大小依然采用-Xms/-Xmx
来指定堆空间大小;堆内存被切割为多个大小固定的区域(Region),而且每个Region是连续的虚拟内存,如图所示每个块内都是连续的;堆默认把内存分为2048个Region,每个Region大小固定,每个Region的取值范围[1m-32m],是2的幂次方。
G1对内存以Region为单位,但对对象的分配是以卡片(Card)为单位,每个Region会被切割为均匀的Card,每个Card大小为512byte,card是堆内存最小颗粒度,所有分区Region的卡片将会记录在全局卡片表中,
Humongous
Humongous是一种对象分配方式,用于处理超大对象。当对象的大小超过了JVM的普通对象分配限制时,也就是分区Region50%以上的对象,JVM将会将这些超大对象分配到一块特殊的内存区域,称为Humongous区域。
- 它被直接分配在老年代,因为超大对象移动成本很高,因此直接在老年代分配
- 超大对象的回收并不是在YoungGC或MixedGC阶段,Humongous被标记为可回收时会等待时机不会立即清除,回收会在两处触发,一次是在FullGC时,还会在全局并发标记阶段的Cleanup阶段回收大对象(后文会讲);
为什么还要采用分代技术?
请问:G1把堆内存切割大小均匀的region,为什么还要使用分代技术(将其区分成Eden、Survivor、Old区)?
因为分代的好处是可以从对象的生命周期集中归类,生命周期短的放在年轻代,周期长的放在老年代,从而避免了整堆扫描,降低了GC时间。 G1分代有两大特点:
- 新生代和老年代不再是物理隔离,它们由多个Region组成的集合,因此G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可。另外每个分区Region也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
- 年轻代是不是固定的,是动态变化的。
- Eden的大小范围可以通过参数
-XX:G1NewSizePercent
和-XX:G1MaxNewSizePercent
调整,默认是整堆5%,整堆60%; - 在[整堆5%,整堆60%]的基础上,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数
-XX:MaxGCPauseMills
设定的值,那么增加年轻代的region, 继续给新对象存放,不会马上做YoungGC。 - G1计算回收时间接近参数
-XX:MaxGCPauseMills
设定的值为止。另外eden和survivor的比例也是8:1:1, - 当然,G1依然可以设置固定的年轻代大小(参数
-Xx: NewRatio
、-Xmn
), 但同时暂停目标(-XX:MaxGCPauseMills
)将失去意义,所以一般不要设置,暂停时间是G1高效率回收的关键之一。比如堆大小设置为2G,那么回可能在500ms才满,大于默认200ms,那么就等于等年轻代满才触发YoungGC,回收时间等于摆设。
- Eden的大小范围可以通过参数
盘点G1中分代回收的特点
- YoungGC:YoungGC不会在现有的Eden区满就立即触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数
-XX:MaxGCPauseMills
设定的值,那么增加年轻代的region继续给新对象存放,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills
设定的值,那么就会触发YoungGC。 - MixedGC:不是FulIGC,当老年代的堆占有率达到参数(
-XX:InitiatingHeapOccupancyPercen
)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。 - FulLGC:停止系统程序,采用单线程进行标记、清理和压缩整理,空闲出来的Region供下一次MixedGC使用,这个过程非常耗时。
CardTable
首先来看YoungGC为什么要扫描全堆:注意看下图中年轻代的A很好判断是存活的,因为可以通过GCRoots直接扫描到;但是DE不行,需要扫描整个老年代来确定DE有没有被老年代对象所引用,此过程相当于对全堆扫描了一遍。
YoungGC扫描全堆解决方案是使用CardTabel,card的初始值是0,如果一个卡片中有对象引用了年轻代的对象也就是跨代引用,就将其设置为1,在寻找存活对象时将标记为1卡片加入到GCRoots中,这样就不用扫描整个老年代了。
RSet已记忆集合
以往的跨代引用只有年轻代老年代2块数据的相互引用,可以采用cardtable来解决。但是现在的G1的设计是把堆均匀按region切割为n个,不仅只有CardTable,RSet 主要用来记录哪个 Region 的哪个 Card 上的对象引用了本 Region 中的对象。
每个区域Region都有一个记忆集RSet,列出了从外部指向该区域的引用,RSet中的信息是实时维护的,也就是每次产生外部引用都会立刻记录到RSet中,而不需要等待GC时才产生。还可以理解成CardTable是记录我引用了谁,RSet则是记录谁引用了我,然后来看例子:
- Region1对象A引用了Region2对象C,对象A存在card;Region3对象B引用了Region2对象D,对象B存在card2。
- Region1和Region3中有对象引用了Region2的对象,则Region2的Rset, Rset=Hash Table
- key1=Region1起始地址,Value=
- key2=Region3起始地址,Value=
- 故判断CD是否存活,到RSet里面查看是否有引用关系就知道了。
- 如果没有Rset,如何判断CD是否存活呢?先扫描Region2所有对象,然后再到Region1和Region3判断是否被引用了,最终判断CD被引用存活,相当于扫描整个堆
哪些引用关系需要记录在RSest?
- 老年代引用新生代:YGC在回收新生代时,如果新生代的对象被老年代引用,那么需要标记为存活对象。即此时的根对象有两种,一个是栈空间/全局变量的引用,一个是老年代到新生代的引用。
- 老年代引用老年代:混合GC时,只会回收部分老年代,被回收的老年代需要正确的标记哪些对象存活。
Per Region Table(PRT)
RSet在内部使用Per Region Table (PRT) 记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区的对象引用非常多,那么RSet占用的空间会上升,从而降低分区的可用空间。
为了提高效率,用了动态化的数据结构存储,在PRT中有3种数据结构存储:
- 稀少:直接记录引用对象的card索引
- 细粒度:记录引用对象的region索引
- 粗粒度:只记录引用情况,每个region对 应一个比特位
由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。
一个应用程序,有无数个像这种代码 objX.fieldY = objZ
,一直不停的运行着,大量的引用变更;要维护RSet必定会拖垮应用线程。现在面临的问题是如何高效的写RSet?采用写屏障来处理,如下图
RSet有2种线程和一个全局队列DirtyCardQueueSet (DCQS),来共同维护 。
- 应用线程通过写屏障,找到该字段(fieldY)所在的Card,并设置为Dirty
- 每条应用线程都有一个私有队列,把card放进缓存队列Dirty Card Queue(DCQ),每个私有队列256的长度,当队列满时就把,DCQ的card迁移到DCQS,清空DCQ.
- Refine线程异步读取队列里面的Card的地址,计算出Card所在的Region,最终把card加入region的rset中。
CSet
CSet用于存储不存活的对象,也就是没有存活对象的Region在Evacuation阶段同一手机存放到CSet集合中,在任意一次GC后,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。
年轻代收集CSet只容纳年轻代分区,混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中,具体会在后文讲解。
高并发场景下JVM如何保障线程安全?
tomcat的并发常见场景:堆是对线程共享的,多个线程可以同时申请堆内存空间,tomcat默认是200条线程,当200条线程同时创建对象时,必定造成争抢内存空间。 JVM提前为每条线程分配一小块内存,然后每条线程在创建对象的时候直接在这块内存分配空间,这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的
虚拟机提前从eden划分一块内存出来,它为每条线程分配一块TLAB空间,这样一来每 条线程就有独立的内存空间。如果线程要为对象分配内存空间,就在自己空间分配就好,避免了eden内存共享的安全性问题,提升内存分配效率。
每条线程分配多大的空间呢?
每条线程分配的空间=eden区* 1% /总线程数(简单粗暴的大概公式,实际上每个垃圾回收器都会做调整而且是动态调整,比较复杂)
如何在TLAB分配对象?
虚拟机设定了一个阀值refill _waste (最大浪费空间),参数: -XX: TLABRefillWasteFraction
(默认值为64, 即表示使用约为1/64空间大小),例如TLAB=1024,那么refill_waste=1024/64=16
情况一:当剩余空间大于16k时,当分配的对象大于16k时,直接在eden区分配。当分配的对象小于16k时,在TLAB上分配。
情况二:当剩余空间小于16k时,虚拟机直接申请新的TLAB,在新的TLAB上分配对象,老的TLAB就废弃, 等待下一次ygc回收就行。