1. JVM G1 对象分配流程


1.1. 什么叫快速分配?什么叫慢速分配?

分配对象速度快、流程少的就叫做快速分配,分配对象慢,流程多的就叫做慢速分配。

快速分配,就是TLAB分配对象的过程。因为多个线程直接通过自己的TLAB就可以分配对象,不需要加锁,就可以完成多个线程并行去创建对象。

  • 创建快
  • 并发度高
  • 无锁化

new_obj_02

快速分配时,多个线程可以并发的去执行对象分配的操作。如下图:

new_obj_03

慢速分配,是没有办法走快速TLAB分配的分配。因为慢速分配需要加锁,甚至可能要涉及GC过程,所以速度非常慢。

new_obj_04

对象分配流程如下:

  1. TLAB剩余内存太小,无法分配对象,会有不同情况:大于refill_waste,直接堆内存分配;小于refill_waste,但TLAB剩余内存空间不够,会重新分配一个TLAB使用。
  2. 如果无法分配新的TLAB(CAS方式来分配TLAB,分配失败)。会堆加锁再分配一个TLAB,如果能够分配成功,就直接在TLAB分配对象。
  3. 如果不能分配,就会尝试去扩展分区,即再申请一些新的region,成功扩展了region,就分配TLAB,然后分配对象,如果不成功就会走垃圾回收。
  4. 如果分配尝试的次数超过了某个阈值(默认为2次),就直接结束,OOM。

1.2. 慢速分配是什么?有几种情况?

慢速分配和快速分配相比多了一些流程,在对象创建上没有效率区别。慢速之所以称为慢速,因为在分配对象的时候,需要去申请内存空间、加锁、还可能涉及到垃圾回收等耗时的操作

慢速分配大概有两种情况:

  • 本来走TLAB,但TLAB空间不够,要重新申请TLAB,并且TLAB初步申请失败。就是,refill_waste这关已经过了,TLAB中对象太多,导致对象放不下。会触发新创建TLAB,进入慢速分配。这个过程的慢速分配是指:慢速分配一个TLAB。

    这种情况,有两种慢速分配的两种场景:

    • 在分配新的TLAB失败之后就进入慢速分配TLAB。经过CAS分配也没成功,只能去尝试拓展新生代,经过GC再次分配一个TLAB的过程。

      new_obj_06

    • 没办法分配TLAB了。此时慢速分配和前面的流程基本相似,但是分配的是对象。

  • 上来判断就无法走TLAB,只能走堆内存分配对象。对象太大,refill_waste这关没过,导致不走TLAB分配,此时会触发慢速分配,直接在堆内存,也就是eden区去分配对象。这个过程的慢速分配是指:慢速分配一个对象。

    new_obj_05

所以,TLAB方式进入慢速分配,是第一次分配TLAB失败,进入一个慢速分配TLAB的的过程。更慢的慢速分配,是TLAB尽力尝试以后,还是无法分配,只能再次进入堆内存慢速分配的过程,两个都叫做慢速分配。


1.3. 大对象分配会走TLAB吗?属于快速分配还是慢速分配?

TLAB的大小和大对象是相关的。大对象的定义是大于regionSize的一半。就是ObjSize > regionSize/2的时候,就可以称为大对象。

大对象的分配有一个特点,不走新生代分配。直接存储在大对象的分区中,在一些书里面说,直接分配到老年代。

  • 大对象太大,并且存活时间可能很长
  • 大对象数量少

上面这两条,是大对象的特点,如果大对象在新生代,那在GC的时候需要复制来复制去,并且占用的空间也大,每次GC大概率回收不掉。大对象数量相对也比较少,所以分配到一个单独的区域来管理更合理。

G1在设计TLAB的时候就考虑到大对象的问题,把TLAB的最大值,限定为regionSize / 2,这样,大对象一定会大于TLAB的大小,就可以直接走慢速分配,到对应的region里面去。

new_obj_07

系统产生的大对象一般是比较少的,一个大对象能直接占满TLAB,会造成其他普通的对象需要进入慢速分配。大对象量不大,但是会占用多个TLAB,并导致其它大量的对象可能重新分配新的TLAB,降低系统的整体效率。在GC的时候,一个大对象引用的数据可能比较多,引用它的可能也比较多,GC的时候不太方便去标记,并它成为垃圾对象的概率也小,复制来复制去,也很耗性能。

综上所述,大对象直接走慢速分配,更能提升效率。


1.4. 大对象的慢速分配特点和普通的慢速分配区别

大对象和TLAB中的慢速分配基本类似。区别就是对象大小的区别,造成分配过程稍微有一些不同,以及大对象分配前,会尝试进行垃圾回收。步骤如下:

  • 大对象分配的时候,会先尝试进行垃圾回收(ygc 或者 mixed gc),同时启动并发标记。(注意:是尝试判断是否需要GC、是否需要启动并发标记。需要才会启动。不需要就不会启动。)
  • 大对象大于HeapRegionSize的一半,但小于一个分区的大小,此时一个完整的分区就能放得下,可以直接从一个空闲列表使用一个分区来用。或者空闲列表里面没有,就分配一个新的堆分区 --- 扩展堆分区。
  • 大对象的大小大于一个完整分区的大小,此时需要分配多个对分区来使用。
  • 上面分配过程失败,就尝试垃圾回收,然后再尝试分配。
  • 最终成功分配;或者失败达到一定次数,则分配失败。

new_obj_08


1.5. 对象分配流程:大概率会快速成功 + 慢速尝试

一般内存不够,会扩展region,基本能够来对象分配。空间实在是不够,才会尝试GC,GC之后再去做分配。极端情况下,才会出现多次分配都失败的情况。

new_obj_09

图中的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 过程比较复杂,流程图如下:

new_obj_10

  • 尝试扩展分区,如果成功,就分配对象然后结束。
  • 不成功的时候,会进行一个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处理,所以过程所需要的时间非常长。


1.8. new一个对象的流程是什么?

new_obj_01

results matching ""

    No results matching ""