1. JVM G1 对象分配流程
1.1. 什么叫快速分配?什么叫慢速分配?
分配对象速度快、流程少的就叫做快速分配,分配对象慢,流程多的就叫做慢速分配。
快速分配,就是TLAB分配对象的过程。因为多个线程直接通过自己的TLAB就可以分配对象,不需要加锁,就可以完成多个线程并行去创建对象。
- 创建快
- 并发度高
- 无锁化
快速分配时,多个线程可以并发的去执行对象分配的操作。如下图:
慢速分配,是没有办法走快速TLAB分配的分配。因为慢速分配需要加锁,甚至可能要涉及GC过程,所以速度非常慢。
对象分配流程如下:
- TLAB剩余内存太小,无法分配对象,会有不同情况:大于refill_waste,直接堆内存分配;小于refill_waste,但TLAB剩余内存空间不够,会重新分配一个TLAB使用。
- 如果无法分配新的TLAB(CAS方式来分配TLAB,分配失败)。会堆加锁再分配一个TLAB,如果能够分配成功,就直接在TLAB分配对象。
- 如果不能分配,就会尝试去扩展分区,即再申请一些新的region,成功扩展了region,就分配TLAB,然后分配对象,如果不成功就会走垃圾回收。
- 如果分配尝试的次数超过了某个阈值(默认为2次),就直接结束,OOM。
1.2. 慢速分配是什么?有几种情况?
慢速分配和快速分配相比多了一些流程,在对象创建上没有效率区别。慢速之所以称为慢速,因为在分配对象的时候,需要去申请内存空间、加锁、还可能涉及到垃圾回收等耗时的操作。
慢速分配大概有两种情况:
本来走TLAB,但TLAB空间不够,要重新申请TLAB,并且TLAB初步申请失败。就是,refill_waste这关已经过了,TLAB中对象太多,导致对象放不下。会触发新创建TLAB,进入慢速分配。这个过程的慢速分配是指:慢速分配一个TLAB。
这种情况,有两种慢速分配的两种场景:
在分配新的TLAB失败之后就进入慢速分配TLAB。经过CAS分配也没成功,只能去尝试拓展新生代,经过GC再次分配一个TLAB的过程。
没办法分配TLAB了。此时慢速分配和前面的流程基本相似,但是分配的是对象。
上来判断就无法走TLAB,只能走堆内存分配对象。对象太大,refill_waste这关没过,导致不走TLAB分配,此时会触发慢速分配,直接在堆内存,也就是eden区去分配对象。这个过程的慢速分配是指:慢速分配一个对象。
所以,TLAB方式进入慢速分配,是第一次分配TLAB失败,进入一个慢速分配TLAB的的过程。更慢的慢速分配,是TLAB尽力尝试以后,还是无法分配,只能再次进入堆内存慢速分配的过程,两个都叫做慢速分配。
1.3. 大对象分配会走TLAB吗?属于快速分配还是慢速分配?
TLAB的大小和大对象是相关的。大对象的定义是大于regionSize的一半。就是ObjSize > regionSize/2的时候,就可以称为大对象。
大对象的分配有一个特点,不走新生代分配。直接存储在大对象的分区中,在一些书里面说,直接分配到老年代。
- 大对象太大,并且存活时间可能很长
- 大对象数量少
上面这两条,是大对象的特点,如果大对象在新生代,那在GC的时候需要复制来复制去,并且占用的空间也大,每次GC大概率回收不掉。大对象数量相对也比较少,所以分配到一个单独的区域来管理更合理。
G1在设计TLAB的时候就考虑到大对象的问题,把TLAB的最大值,限定为regionSize / 2,这样,大对象一定会大于TLAB的大小,就可以直接走慢速分配,到对应的region里面去。
系统产生的大对象一般是比较少的,一个大对象能直接占满TLAB,会造成其他普通的对象需要进入慢速分配。大对象量不大,但是会占用多个TLAB,并导致其它大量的对象可能重新分配新的TLAB,降低系统的整体效率。在GC的时候,一个大对象引用的数据可能比较多,引用它的可能也比较多,GC的时候不太方便去标记,并它成为垃圾对象的概率也小,复制来复制去,也很耗性能。
综上所述,大对象直接走慢速分配,更能提升效率。
1.4. 大对象的慢速分配特点和普通的慢速分配区别
大对象和TLAB中的慢速分配基本类似。区别就是对象大小的区别,造成分配过程稍微有一些不同,以及大对象分配前,会尝试进行垃圾回收。步骤如下:
- 大对象分配的时候,会先尝试进行垃圾回收(ygc 或者 mixed gc),同时启动并发标记。(注意:是尝试判断是否需要GC、是否需要启动并发标记。需要才会启动。不需要就不会启动。)
- 大对象大于HeapRegionSize的一半,但小于一个分区的大小,此时一个完整的分区就能放得下,可以直接从一个空闲列表使用一个分区来用。或者空闲列表里面没有,就分配一个新的堆分区 --- 扩展堆分区。
- 大对象的大小大于一个完整分区的大小,此时需要分配多个对分区来使用。
- 上面分配过程失败,就尝试垃圾回收,然后再尝试分配。
- 最终成功分配;或者失败达到一定次数,则分配失败。
1.5. 对象分配流程:大概率会快速成功 + 慢速尝试
一般内存不够,会扩展region,基本能够来对象分配。空间实在是不够,才会尝试GC,GC之后再去做分配。极端情况下,才会出现多次分配都失败的情况。
图中的1、2、3步就是拓展、回收的过程。大部分情况,在1、3步会直接成功。
如果通过TLAB分配对象,拓展一个新的TLAB基本就会成功,不会到垃圾回收这一步。如果不成功,直接堆内存分配(此时是慢速分配)、拓展分区来分配。还不成功,才尝试触发ygc,然后再尝试分配,如果还是无法成功就只能返回失败。经历的gc包括:ygc,mixedgc,最终的拯救环节。
1.6. 慢速分配失败以后,G1会怎么拯救?
在慢速分配,快速分配的过程中,会尝试gc,ygc 或者 mixedgc,说明,还有可能有空间来利用。即使失败,最终会有 Full GC 来保底。
1.6.1. Full GC
Full GC 过程比较复杂,流程图如下:
- 尝试扩展分区,如果成功,就分配对象然后结束。
- 不成功的时候,会进行一个GC(注意:这次GC是Full GC,但是这次GC,不回收软引用)。回收之后,再次分配对象,如果成功就结束。
- 如果还不成功,再进行一次 Full GC,这次要把软引用回收掉。然后再次尝试分配对象,成功就结束;如果不成功,就会OOM。
从上面的流程可以看出,一次OOM,很有可能会出现大量的gc(看gc日志,发现oom之前会有好几次gc纪录)。
1.7. 总结
对象分配涉及到的GC过程不同阶段是不一样的。
使用TLAB进行快速分配的过程,第一次进入慢速分配,扩展空间失败的时候,就是ygc 或者 mixed gc。
再次进入慢速分配,有可能还会执行gc,在分配过程中执行的也是 ygc 或者 mixed gc。
慢速分配也失败的时候,就会进入最终的尝试,最终尝试会执行两次 full gc,一次不回收软引用,一次回收软引用。
对象分配大部分都是快速分配,慢速分配的场景比较少。一般是TLAB大小不合理造成的短暂的慢速分配,或者是大对象的分配直接进入慢速分配。在慢速分配的过程中,因为要做很多扩展、加锁、甚至gc处理,所以过程所需要的时间非常长。