G1原理—2.G1是如何提升分配对象效率

大纲

1.G1的对象分配原理是怎样的

2.深入分析TLAB机制原理

3.借助TLAB分配对象的实现原理是什么

4.什么是快速分配 + 什么是慢速分配

5.大对象分配的过程 + 与TLAB的关系

6.救命的稻草---JVM的最终分配尝试

G1如何分配对象+TLAB机制+分区协调机制

G1设计了一套TLAB机制+快速分配机制用来提升分配对象的效率

G1设计了一套记忆集+位图+卡表+DCQ+DCQS机制用来提升垃圾回收的效率

1.G1的对象分配原理是怎样的

(1)停顿预测模型总结

(2)G1中是怎么分配一个对象的

(3)如何解决对象创建过程的冲突问题

(4)无锁化分配------基于TLAB的快速分配

(5)分配TLAB时对堆内存加锁------大大减少锁冲突导致串行化执行的问题

G1除了要考虑垃圾对象回收的效率外,还要考虑对象分配的效率。如果对象分配很慢,那即便对象垃圾回收效率很高,系统性能也不高。

(1)停顿预测模型总结

G1如何满足用户设定的停顿时间?

一.预测在停顿时间范围内G1能回收多少垃圾

二.G1进行预测的依据其实就是历史数据

三.拿到历史数据后G1应该怎么样

四.线性算法模型、衰减算法模型

如何设计一个合理的预测算法?

通过衰减标准差算法:

davg(n) = Vn, n = 1
davg(n) = (1 - α) * Vn + α * davg(n - 1), n > 1
//上述公式中的α为历史数据权值,1-α为最近一次数据权值
//衰减因子α越小,最新的数据对结果影响越大,最近一次的数据对结果影响最大


//例如α = 0.6,GC次数为3,三次分别为:
//第一次回收2G,用时200ms
//第二次回收5G,用时300ms
//第三次回收3G,用时500ms
//那么计算结果就如下:
davg(1) = 2G / 200ms
davg(2) = (1 - 0.6) * 5G / 300ms + 0.6 * 2G / 200ms
davg(3) = (1 - 0.6) * 3G / 500ms + 0.6((1 - 0.6) * 5G / 300ms + 0.6 * 2G / 200ms)

(2)G1中是怎么分配一个对象的

系统程序在创建一个对象时,会先找新生代的Eden区来存储。在G1中,会从Eden区包含的Region里选择一个Region来进行对象的分配。

但是如果有两个线程,同时要找其中一个Region来分配对象,并且这两线程刚好找到这个Region里的同段内存,那么就会出现并发安全问题。

(3)如何解决对象创建过程的冲突问题

一个简单的思路就是加锁。线程1在分配对象时,直接对整个堆内存加锁。分配完成后,再由线程2进行对象分配,此时一定不会出现并发安全问题。

为什么要对整个堆内存进行加锁?因为对象分配的过程是非常复杂的,不仅仅是分配一个对象。还要做引用替换、引用关系处理、Region元数据维护、对象头处理等。只锁一个Region,或只锁一段内存是不够的,因此只能锁整个堆内存。

但是新的问题出现了,这个分配效率很显然非常低,那么应该如何解决这个分配的效率问题?

(4)无锁化分配------基于TLAB的快速分配

想要解决并发安全问题,一般有三种思路:

一.使用锁

二.使用CAS这种自旋模式(类似锁的思想)

三.使用本地缓冲,自己改自己的

G1会使用本地缓冲区来解决并发分配对象的安全和效率问题,整体来说G1提供了两种对象分配策略:

一.慢速分配

二.基于线程本地分配缓冲(TLAB)的快速分配

TLAB全称就是Thread Local Allocation Buffer,即线程本地分配缓冲。每个线程都会有一个自己的本地分配缓冲区,专门用于对象的快速分配。所以TLAB产生的目的就是为了进行内存快速分配,G1会通过为每个线程分配一个TLAB缓冲区来避免和减少使用锁。

TLAB属于线程的,不同的线程不共享TLAB。线程在分配对象时,会从JVM堆分配一个固定大小的内存区域作为TLAB。然后优先从当前线程的TLAB中分配对象,不需要锁,从而实现无锁化分配即快速分配。

(5)分配TLAB时对堆内存加锁------大大减少锁冲突导致串行化执行的问题

一.为什么说TLAB大大减少了锁冲突导致串行化执行的问题

分配TLAB时,由于一个线程会有一个TLAB,为避免多个线程对同一块内存分配TLAB产生并发冲突,会采用CAS自旋。

自旋次数其实可能也就与线程数量一致,基本执行几十次最多几百次。一个for循环里执行几十次几百次是很快的,连1ms都不到。

这相当于不再把锁放在分配对象的环节,因为分配对象可能达上千万次。而TLAB就相当于把上千万次的加锁过程,减少到几十次到几百次,所以就大大减少了锁冲突导致串行化执行的问题。

如图所示,只有在线程需要分配TLAB时才会对堆内存加一个全局锁。如果不需要分配TLAB就直接快速分配一个对象,这样就大大提升了效率。

二.其他的一些问题

既然要分配TLAB,那何时分配TLAB、分配多大、TLAB占满了怎么办?如果实在没有办法用TLAB的方式分配,有没有什么兜底的策略?

TLAB不能无限大,一定会有被占满的时候。并且TLAB被占满了以后,程序肯定要继续运行,这时该怎么办?

2.深入分析TLAB机制原理

(1)TLAB是什么 + TLAB是怎么分配的

(2)如何确定TLAB大小 + TLAB满了如何处理

(3)怎么判断TLAB满了

(4)TLAB满了怎么办 + 经常满又怎么办

(1)TLAB是什么 + TLAB是怎么分配的

首先需要知道的是,程序创建的对象是由线程创建的。线程在分配时,也是以一个对象的身份分配出来的,比如创建线程是由Thread对象new出来的:

Thread thread = new Thread();

所以创建一个线程时,也会有一个线程对象需要被分配出来。而事实上,分配TLAB就是和分配线程对象同时进行的。

创建线程,分配线程对象时,会从堆内存分配一个固定大小的内存区域。并且将该区域作为线程的私有缓冲区,这个私有缓冲区就是TLAB。

注意:在分配TLAB给线程时,是需要加锁的,G1会使用CAS来分配TLAB。

问题:TLAB的数量不能无限多,应怎么限制?

因为分配线程对象时,会从JVM堆内存上分配一个TLAB供线程使用,所以理论上有多少个线程就会有多少个TLAB缓冲区。那么由于线程数量肯定不会是无限的,否则CPU会崩溃,所以TLAB的数量会跟随线程的数量:有多少个线程,就有多少个TLAB。

问题:如果TLAB过大,会有什么问题?如果TLAB过小,又会有什么问题?

(2)如何确定TLAB大小 + TLAB满了如何处理

一.TLAB的大小要有一个平衡点

情况一:如果TLAB过小

会导致TLAB快速被填满,从而导致不断分配新的TLAB,降低分配效率。

情况二:如果TLAB过大

由于TLAB是线程独享,所以TLAB过大会造成内存碎片,拖慢垃圾回收的效率。因为运行过程中,TLAB可能很多内存都没有被使用,造成内存碎片。同时在垃圾回收时,因为要对TLAB做一些判断,所以会拖慢垃圾回收的效率。

二.如何确定TLAB的大小

TLAB初始化时有一个公式计算:TLABSize = Eden * 2 * 1% / 线程个数。其中乘以2是因为,JVM的开发者默认TLAB的内存使用服从均匀分布。均匀分布指对象会均匀分布在整个TLAB空间,最终50%的空间会被使用。分配好TLAB后,线程在创建对象时,就会优先通过TLAB来创建对象。

三.TLAB满了无法分配对象了会怎么处理

TLAB满了的处理思路无非两种:

一.重新申请一个TLAB给线程继续分配对象

二.直接通过堆内存分配对象

G1是使用了两者结合的方式来操作的。如果TLAB满了无法分配对象了,就先去申请一个新的TLAB来分配对象。如果无法申请新的TLAB,才通过对堆内存加锁,直接在堆上分配对象。

(3)怎么判断TLAB满了

一.为什么需要判断TLAB满了

因为TLAB大小分配好后,其大小就固定了,而对象的大小却是不规则的,所以很有可能会出现对象放不进TLAB的情况。但是TLAB却还有比较大比例的空间没有使用,这时就会造成内存浪费。所以如何判断TLAB满了,是一个比较复杂的事情。

二.G1是怎么判断TLAB满了

G1设计了一个refill_waste来判断TLAB满了,refill_waste的含义是一个TLAB可以浪费的最大内存大小是refill_waste。也就是说,一个TLAB中最多可以剩余refill_waste这么多的空闲空间。如果TLAB剩余的空闲空间比refill_waste少,那就代表该TLAB已经满了。

refill_waste的表示一个TLAB中可以浪费的内存的比例,refill_waste的值可以通过TLABRefillWasteFraction来调整。TLABRefillWasteFraction默认值是64,即可以浪费的内存比例为1/64。如果TLAB为1M,那么refill_waste就是16K。

问题:判断一个TLAB满了以后,对象应该怎么分配?如果TLAB经常进入这种满的状态,说明TLAB的空间设置不是很合理,和我们对象大小的规律不匹配了,应该怎么解决这个不合理?

(4)TLAB满了怎么办 + 经常满又怎么办

G1设计的refill_waste不是简单去判断是否满了,其判断过程会比较复杂,具体逻辑如下:

一.线程要分配一个对象,首先会从线程持有的TLAB里面进行分配

如果TLAB剩余空间够了,就直接分配。如果TLAB剩余空间不够,这时就去判断refill_waste。

二.此时要对比对象所需空间大小是否大于refill_waste这个最大浪费空间

如果大于refill_waste,则直接在TLAB外分配,也就是在堆内存里直接分配。如果小于refill_waste,就重新申请一个TLAB,用来存储新创建的对象。

三.重新申请新的TLAB时,会根据TLABRefillWasteFraction来动态调整

动态调整目的是适应当前系统分配对象的情况,动态调整依据是refill_waste和TLAB大小无法满足当前系统的对象分配。因为对象既大于当前TLAB剩余的可用空间,也大于refill_waste。即剩余空间太小了,分配对象经常没办法分配,只能到堆内存加锁分配。所以很显然还没有达到一个更加合理的refill_waste和TLAB大小。因此系统会动态调整TLAB大小和refill_waste大小,直到一个更合理的值。

3.借助TLAB分配对象的实现原理是什么

(1)TLAB是怎么实现分配对象的(指针碰撞法)

(2)dummy哑元对象的作用是处理TLAB内存碎片

(3)如果实在无法在TLAB分配对象,应该怎么处理

(1)TLAB是怎么实现分配对象的(指针碰撞法)

对象分配是一个比较复杂的过程,这里我们不关注对象到底怎么创建的,因为它包含了很多东西:比如引用、对象头、对象元数据、各种标记位、对象的klass类型对象、锁标记、GC标记、Rset、卡表等。

一.TLAB是怎么实现分配一个对象的

分配一个对象时,TLAB是只给当前这一个线程使用的,因此当前线程可以直接找到这个TLAB进行对象的分配。

那么此时就需要知道TLAB是不是满了、或者对象能不能放得下。如果TLAB剩余内存能放得下,就创建对象。如果TLAB剩余内存放不下就进行如下图示的流程:要么直接堆内存创建对象、要么分配新的TLAB给线程,再继续创建对象。

可见对象在TLAB中能不能放得下是很关键的,那么TLAB中用了什么机制来判断能不能放得下的?

二.TLAB是怎么判断对象能否放得下的

一个比较简单的思路是先确定分配出去了哪些空间。由于TLAB是一个很小的空间,而且对象的分配是按照连续内存来分配的,所以可以直接遍历整个TLAB,然后找到第一个没有被使用的内存位置。接着用TLAB结束地址减去第一个没有被使用的内存地址,得到剩余大小,再将TLAB剩余大小和对象大小进行比较。

但这个思路有一个问题:每一次对象分配都要遍历TLAB,是否有必要?其实每次分配新对象的起始地址,就是上一次分配对象的结束地址。所以可以用一个指针(top指针),记下上次分配对象的结束地址,然后下次直接用这个作为起始位置进行直接分配。

如下图示:在分配对象obj3时,TLAB里的top指针记录的就是obj2对象的结束位置。

当obj3分配完成时,此时就把指针更新一下,更新到最新的位置上去。

但是分配对象时肯定不能直接进入TLAB去分配,因为有可能空间会不够用。所以在分配对象时会判断一下剩余内存空间是否能够分配这个对象。

那么具体应该怎么判断剩余内存空间是否能够分配这个对象呢?此时就需要记录一下整个TLAB的结束位置(end指针)。这样在分配对象时,对比下待分配对象的空间(objSize)和剩余的空间即可。

知道end指针位置,那么判断关系就很容易:

如果objSize <= end - top,可分配对象。

如果objSize > end - top,不能分配对象。

问题:因为TLAB是一个固定的长度,而对象很有可能有的大有的小,所以有可能会产生一部分内存空间无法被使用的情况,也就是产生了内存碎片,那么这个内存碎片应该怎么处理呢?

(2)dummy哑元对象的作用是处理TLAB内存碎片

由于TLAB不大,TLAB大小的计算公式是:(Eden * 2 * 1%)/ 线程个数。所以如果TLAB有内存碎片,实际上也就是比一个普通小对象的大小还要小一点。

对于一个系统来说:可能几百个线程,总共加起来的内存碎片也就几百K到几M之间。所以为了这么小的空间,专门设计一个内存压缩机制,肯定是不太合理的。而且也不太好压缩,因为每个线程都是独立的TLAB。把所有对象压缩一下,进入STW,然后把对象集中放到线程的TLAB吗?如果对象在线程1的TLAB分配,压缩后出现在线程2的TLAB里面,那此时该对象应该由谁管理,所以压缩肯定是不合理的。

所以这块小碎片如果对内存的占用不大,能否直接放弃掉?答案是可以的,而G1也确实是这么做的,这块内存碎片直接放弃不使用。而且在线程申请一个新的TLAB时,这个TLAB也会被废弃掉。这个废弃指的不是直接销毁,而是不再使用该TLAB,进入等待GC状态。

此时会有一个新的问题:在GC时,遍历一个对象,是可以直接跳过这个对象长度的内存的。因为对象属性信息中有对象长度,故遍历对象时拿到对象长度就可跳过。但是TLAB里的小碎片,由于没有对象属性信息,所以不能直接跳过。只能把这块小碎片的内存一点一点进行遍历,这样性能就会下降。

所以G1使用了填充方式来解决遍历碎片空间时性能低下的问题,G1会直接在碎片里面填充一个dummy对象。这样GC遍历到这块内存时:就可以按照dummy对象的长度,跳过这块碎片的遍历。

问题:如果没有办法用TLAB分配对象,那么此时应该怎么办?新建一个TLAB?那么如果新建一个TLAB失败了,怎么办?

(3)如果实在无法在TLAB分配对象,应该怎么处理

一.对旧TLAB填充dummy对象

TLAB剩余内存太小,无法分配对象,会有不同情况:如果对象大于refill_waste,直接通过堆内存分配。如果对象小于refill_waste,这时会重新分配一个TLAB来用。在重新分配一个TLAB之前,会对旧的TLAB填充一个dummy对象。

二.分配新TLAB时先快速无锁(CAS)分配再慢速分配(堆加锁)

重新分配一个TLAB时,先进行快速无锁分配(CAS),再进行慢速分配(堆加锁)。

快速无锁分配(CAS):如果通过CAS重新分配一个新TLAB成功,也就是Region分区空间足够使CAS分配TLAB成功,则在新TLAB上分配对象。

慢速分配(堆加锁):如果通过CAS重新分配一个新TLAB失败,则进行堆加锁分配新TLAB。如Region分区空间不足导致CAS分配TLAB失败,需要将轻量级锁升级到重量级锁。

三.堆加锁分配时可能扩展Region分区

进行堆加锁分配一个新的TLAB时:如果堆加锁分配一个新TLAB成功,就在Region上分配一个新的TLAB(堆加锁分配TLAB成功)。如果堆加锁分配一个新TLAB失败,就尝试扩展分区,申请新的Region(堆加锁分配TLAB失败)。

四.扩展Region分区时可能GC + OOM

扩展分区成功就继续分配对象,扩展分区失败就进行GC垃圾回收。如果垃圾回收的次数超过了某个阈值,就直接结束报OOM异常。

解释一下最后的这个垃圾回收:

如果因为内存空间不够,导致无法分配对象时,那么肯定需要垃圾回收。如果垃圾回收后空间还是不够,说明存活对象太多,堆内存实在不够了。这时程序肯定无法分配对象、无法运行,所以准备OOM。那么OOM前,可能还会尝试几次垃圾回收,直到尝试次数达到某个阈值。比如达到了3次回收还是无法分配新对象,才会OOM。

4.什么是快速分配 + 什么是慢速分配

(1)什么叫快速分配 + 什么叫慢速分配

(2)慢速分配是什么 + 有几种情况

(1)什么叫快速分配 + 什么叫慢速分配

分配对象速度快、流程少的就叫快速分配。

分配对象速度慢、流程多的就叫慢速分配。

**快速分配:**TLAB分配对象的过程就叫做快速分配。多个线程通过TLAB就可以分配对象,不需要加锁就可以并行创建对象。TLAB分配对象具有的特点:创建快、并发度高、无锁化。

**慢速分配:**没有办法使用TLAB快速分配的就是慢速分配。因为慢速分配需要加锁,甚至可能要涉及GC过程,分配的速度会非常慢。

整个对象分配流程如下,注意上图中的慢速分配包括:慢速TLAB分配 + 慢速对象分配

一.TLAB剩余内存太小,无法分配对象,则判断refill_waste

如果对象大小大于refill_waste,直接通过堆内存分配,不进行TLAB分配。如果对象大小小于refill_waste,这时会重新分配一个TLAB。

二.进行重新分配一个TLAB时,会通过CAS来分配一个新的TLAB

如果CAS分配成功,则在新的TLAB上分配对象(快速无锁分配)。如果CAS分配失败,就会对堆内存加锁再去分配一个TLAB(慢速分配)。如果堆内存加锁分配新TLAB成功,则可直接在新的TLAB上分配对象。

三.如果堆内存加锁分配失败,就尝试扩展分区,再申请一些新的Region

成功扩展了Region就分配TLAB,然后分配对象,如果不成功就进行GC。

四.如果GC的次数超过了阈值(默认为2次),就直接结束报OOM异常

问题:什么情况下会出现慢速分配,有几种慢速分配的情况?

(2)慢速分配是什么 + 有几种情况

慢速分配其实和快速分配相比起来就是多了一些流程,在对象创建这个层面上是没有效率区别的。

慢速之所以称为慢速,是因为在分配对象时:需要进行申请内存空间、加锁等一系列耗时的操作,并且慢速分配除了会加锁,还可能涉及到垃圾回收的过程。

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

情况一:TLAB空间不够,要重新申请TLAB,但CAS申请TLAB失败了

这种情况就是refill_waste判断已通过,TLAB中对象太多,导致对象放不下。此时会创建新的TLAB,但是CAS分配TLAB失败,于是慢速分配TLAB。这个过程的慢速分配是指:慢速分配一个TLAB。

情况二:判断无法进行TLAB分配,只能通过堆内存分配对象

这种情况就是refill_waste判断没通过,对象太大了,导致不能进行TLAB分配。此时会触发慢速分配,并且不是去申请TLAB,而是直接进入慢速分配。也就是直接在堆内存的Eden区去分配对象,这个过程的慢速分配是指慢速分配一个对象。

慢速分配的两种情况如下图示:

所以快速TLAB分配失败后进入的慢速分配,是个慢速分配TLAB的过程。随后可能会发生更慢的慢速分配,即慢速分配TLAB失败,此时会GC。

问题:上面一直说的对象分配,默认认为对象可以在整个TLAB中放得下。那么如果有一个大对象,整个TLAB都根本放不下了,怎么办?此时的对象分配是快速还是慢速?

5.大对象分配的过程 + 与TLAB的关系

(1)大对象分配会使用TLAB吗 + 它属于快速分配还是慢速分配

(2)大对象的慢速分配有什么特点 + 和普通的慢速分配有没有什么区别

(1)大对象分配会使用TLAB吗 + 它属于快速分配还是慢速分配

要确定大对象能不能进行TLAB分配,首先得知道TLAB的大小,TLAB的大小和大对象是相关的。

一.什么是大对象 + 大对象的特点

大对象的定义:

如果一个对象的大小大于RegionSize的一半,那么这个对象就是大对象。也就是ObjSize > RegionSize / 2的时候,就可以称它为大对象。

大对象的分配:

大对象不会通过新生代来分配,而是直接分配在大对象的Region分区中。问题:为什么它要直接存储在大对象的分区中?不经过新生代?

大对象的特点:

一.大对象太大,并且存活时间可能很长

二.大对象数量少

二.大对象能否在新生代分配 + TLAB的上限

如果大对象在新生代分配会怎么样?如果大对象在新生代,那么GC时就会很被动。因为需要来回复制,并且占用的空间还大,每次GC大概率又回收不掉。而且它本身数量相对来说比较少,所以直接将大对象分配到一个单独的区域来管理才比较合理。

G1如何根据大对象的特点来设计TLAB上限?由于大对象的ObjSize > RegionSize / 2,所以G1把TLAB的最大值限定为RegionSize / 2,这样大对象就一定会大于TLAB的大小。然后就可以直接进入慢速分配,到对应的Region里去。

G1设定TLAB最大值为大对象最小值的原因总结:

原因一:大对象一般比较少,如果进入TLAB则会导致普通对象慢速分配

一个系统产生的大对象一般是比较少的,如果一个大对象来了就占满TLAB了或占用多个TLAB,那么会造成其他普通对象需要进入慢速分配。大对象占满了TLAB后,其他对象就需要重新分配新的TLAB,这就降低系统的整体效率;

原因二:在GC时不方便标记大对象

一个大对象引用的东西可能比较多,引用它的可能也比较多,所以GC时不太方便去标记大对象;

原因三:大对象成为垃圾对象的概率小,不适合在GC过程中来回复制

新生代GC不想管大对象,并且管理起来影响效率,所以新生代最好是不管大对象的。因此干脆让大对象直接进行慢速分配,反而能提升一些效率。所以G1设定TLAB上限就是Region的一半大小,TLAB上限即大对象下限,这个设定就会让大对象直接进行慢速分配。

(2)大对象的慢速分配有什么特点 + 和普通的慢速分配有没有什么区别

大对象和TLAB中的慢速分配类似,区别是:

区别一:大对象分配前会尝试进行垃圾回收

区别二:大对象可能因大小的不同,造成分配过程稍微有一些不同

大对象的慢速分配步骤如下:

步骤一:先判断是否需要GC,需要则尝试垃圾回收 + 启动并发标记

和普通对象的慢速分配不同点在于:大对象分配时,先判断是否需要GC,是否需要启动并发标记,如果需要则尝试进行垃圾回收(YGC或Mixed GC) + 启动并发标记。

步骤二:如果大对象大于HeapRegionSize的一半,但小于一个分区的大小

此时一个完整分区就能放得下,可以直接从空闲列表拿一个分区给它。或者空闲列表里面没有,就分配一个新的Region分区,扩展堆分区。

步骤三:如果大对象大于一个完整分区的大小,此时就要分配多个Region分区

步骤四:如果上面的分配过程失败,就尝试垃圾回收,然后再继续尝试分配

步骤五:最终成功分配,或失败到一定次数分配失败

问题:如果失败了就GC,尝试达到了某个次数就分配失败。那么失败了以后,JVM就直接OOM了吗?如果不是OOM,有没有什么方式补救。

6.救命的稻草---JVM的最终分配尝试

(1)慢速分配总结

(2)大概率会成功的快速 + 慢速尝试

(3)慢速分配失败以后G1会怎么拯救

(4)FGC在哪里触发 + 会执行几次 + 执行的过程中会做什么操作

(5)总结

(1)慢速分配总结

一.慢速分配是什么 + 快速分配是什么

二.慢速分配有几种场景

三.慢速分配的流程是什么

四.大对象的分配属于什么流程

(2)大概率会成功的快速 + 慢速尝试

一般即使内存不够,扩展一下Region,就能获取足够内存做对象分配了。实在不够才会尝试GC,GC之后继续去做分配。

其实百分之九十九点九的概率是可以成功分配的,极端情况下才会出现尝试了好多次分配,最后都还是失败了的情形。

上图中的1、2、3步就是扩展、回收的过程,很多情况下直接在1、3步就直接成功了。比如通过TLAB去分配对象,那么其实扩展一个新的TLAB就基本成功了,不会走到垃圾回收这一步。

如果扩展TLAB不成功,那么就直接堆内存分配(慢速分配)、扩展分区。如果堆内存分配 + 扩展分区还是不成功,才会尝试触发YGC,再来一次。如果再来一次还是无法成功就只能返回失败了,那么返回失败之后就直接OOM了吗?没有挽救的余地了吗?前面的失败,经历的GC都是先YGC或Mixed GC,然后进入拯救环节。

(3)慢速分配失败以后G1会怎么拯救

首先需要明确:在慢速分配的过程中,肯定是会尝试去GC的,但是触发的GC要么是YGC要么是Mixed GC。那就说明,还没有到山穷水尽的地步,因为还有一个FGC没有用。

所以慢速分配失败后肯定不是直接OOM,而会有一个最终的兜底过程。这个过程会进入最恐怖的FGC过程,是极慢极慢的。

那这个过程到底会做什么?FGC会在哪里触发?会执行几次?执行的过程中会做什么操作?

(4)FGC在哪里触发 + 会执行几次 + 执行的过程中会做什么操作

如果上面的过程结束后还是没有返回一个对象,代表慢速分配也失败了。过程中进行的GC也无法腾出空间,那就会走向最后一步,触发FGC。这个GC过程会比较复杂,流程图如下:

一.尝试扩展分区成功就可以分配对象。

二.如果尝试扩展分区不成功,则会进行一次GC。注意这次GC是FGC,但是这次GC不回收软引用。这次GC后会再次尝试分配对象,如果成功了就结束。

三.如果尝试分配对象还是不成功,就进行FGC。这次FGC会把软引用回收掉,然后再次尝试分配对象。如果再次分配对象成功了,就结束返回。如果再次分配对象还是不成功,就只能OOM,无法挽救。

从上面的流程可以看出:假如一次对象分配失败造成了OOM,很有可能会出现大量GC。这也符合有时看GC日志会发现OOM前多了好几次GC记录的情况。

(5)总结

总的来说,对象分配涉及到的GC过程,在不同的阶段是不一样的。比如在使用TLAB进行快速分配的过程中:第一次进入慢速分配,扩展分区失败时,就是YGC或者Mixed GC。再次进入慢速分配,有可能还会执行YGC或者Mixed GC(没达阈值)。当慢速分配也失败时,才会进行最终的尝试。在最终的尝试中,会尝试执行两次FGC。第一次FGC不回收软引用,第二次FGC会回收软引用。

另外,对象分配一般都是进入快速分配,慢速分配的场景比较少:一般是TLAB大小不合理造成短暂慢速分配,或者是大对象的分配直接进入慢速分配。

慢速分配的过程需要的时间非常长,因为要做很多扩展分区的处理、加锁的处理、甚至GC的处理。

相关推荐
工业甲酰苯胺14 分钟前
JVM实战—OOM的定位和解决
服务器·jvm·php
旷野..18 小时前
Java协程的引入会导致GC Root枚举复杂度大大增加,JVM是如何解决的呢?
java·开发语言·jvm
Evaporator Core19 小时前
SQLite 的未来发展与展望
jvm·性能优化·sqlite
蜗牛_snail19 小时前
JVM三JVM虚拟机
jvm
Evaporator Core1 天前
SQLite 调试与性能优化指南
jvm·性能优化·sqlite
工业甲酰苯胺1 天前
JVM实战—OOM的生产案例
jvm
蜗牛_snail1 天前
JVM二运行时数据区
jvm
杨荧1 天前
【开源免费】基于Vue和SpringBoot的贸易行业crm系统(附论文)
前端·javascript·jvm·vue.js·spring boot·spring cloud·开源
东阳马生架构2 天前
G1原理—1.G1回收器的分区机制
jvm