G1 原理—G1 是如何提升分配对象效率
1.G1 的对象分配原理是怎样的
G1 除了要考虑垃圾对象回收的效率外,还要考虑对象分配的效率。如果对象分配很慢,那即便对象垃圾回收效率很高,系统性能也不高。
(1)停顿预测模型总结
G1 如何满足用户设定的停顿时间?
一.预测在停顿时间范围内 G1 能回收多少垃圾
二.G1 进行预测的依据其实就是历史数据
三.拿到历史数据后 G1 应该怎么样
四.线性算法模型、衰减算法模型
如何设计一个合理的预测算法?
通过衰减标准差算法:
(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 是怎么分配的
首先需要知道的是,程序创建的对象是由线程创建的。线程在分配时,也是以一个对象的身份分配出来的,比如创建线程是由 Thread 对象 new 出来的:
所以创建一个线程时,也会有一个线程对象需要被分配出来。而事实上,分配 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 是怎么实现分配对象的(指针碰撞法)
对象分配是一个比较复杂的过程,这里我们不关注对象到底怎么创建的,因为它包含了很多东西:比如引用、对象头、对象元数据、各种标记位、对象的 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)什么叫快速分配 + 什么叫慢速分配
分配对象速度快、流程少的就叫快速分配。
分配对象速度慢、流程多的就叫慢速分配。
快速分配: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 吗 + 它属于快速分配还是慢速分配
要确定大对象能不能进行 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)大概率会成功的快速 + 慢速尝试
一般即使内存不够,扩展一下 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 的处理。
文章转载自:东阳马生架构
评论