G1的对象分配

G1的对象分配

对象分配直接关系到内存的使用效率垃圾回收的效率,不同的分配策略也会影响对象的分配速度.

G1提供了两种对象分配策略
基于线程本地分配缓冲区(Thread Local Allocation Buffer,TLAB)的 快速分配慢速分配.

当不能成功分配对象时就会触发垃圾回收.

对象分配概述

为了提高效率,无论快速分配还是慢速分配,都应该在STW之外调用,即都应该尽量避免使用全局锁.

把内存分配算法设计成几个层次,首先进行无锁分配,再进行加锁,从而尽可能地满足并行化分配。

快速分配

TLAB产生的目的就是为了进行内存快速分配

JVM堆是所有线程的共享区域。
因此,从JVM堆空间分配对象时,必须锁定整个堆,以便不会被其他线程中断和影响。
为了解决这个问题,TLAB 试图通过为每个线程 分配一个缓冲区 来避免和减少使用锁

在分配线程对象时,从JVM堆中分配一个固定大小的内存区域并将其作为线程的私有缓冲区,这个缓冲区称为TLAB

只有在为每个线程分配TLAB缓冲区时才需要锁定整个JVM堆

由于TLAB是属于线程的,不同的线程不共享TLAB,当我们尝试分配一个对象时,优先从当前线程的TLAB中分配对象,不需要锁,因此达到了快速分配的目的。

实际上TLAB是Eden区域中的一块内存,不同线程的TLAB都位于Eden区,所有的TLAB内存对所有的线程都是可见的,只不过每个线程有一个TLAB的数据结构,用于保存待分配内存区间的起始地址(start)和结束地址(end),在分配的时候只在这个区间做分配,从而达到无锁分配,快速分配。

虽然TLAB在分配对象空间的时候是无锁分配,但是TLAB空间本身在分配的时候(分配TLAB内存空间)还是需要锁的,G1中使用了CAS来并行分配。

·T1已经使用完两个TLAB块,这两个块在回收的时候如何处理?

·我们可以想象TLAB的大小是固定的,但是对象的大小并不固定,因此TLAB中可能存在内存碎片的问题,这个该如何解决?

快速TLAB对象分配 也有两步:

·从线程的TLAB分配空间,如果成功则返回。

·不能分配,先尝试分配一个新的TLAB,再分配对象。

从TLAB已分配的缓冲区空间直接分配对象,也称为指针碰撞法分配,其方法非常简单,在TLAB中保存一个top指针用于标记当前对象分配的位置,如果剩余空间(end-top)大于待分配对象的空间(objSize),则直接修改top=top+ObjSize

如果TLAB过小,那么TLAB则不能存储更多的对象,所以可能需要不断地重新分配新的TLAB。但是如果TLAB过大,则可能导致内存碎片问题。

假设TLAB大小为1M,Eden为200M。如果有40个线程,每个线程分配1个TLAB,TLAB被填满之后,发生GC。假设TLAB中对象分配符合均匀分布,那么发生GC时,TLAB总的大小为:40×1×0.5=20M(Eden的10%左右),这意味着Eden还有很多空间时就发生了GC,这并不是我们想要的

最直观的想法是增加TLAB的大小或者增加线程的个数,这样TLAB在分配的时候效率会更高,但是在GC回收的时候则可能花费更长的时间。

因此JVM提供了参数TLABSize用于控制TLAB的大小,如果我们设置了这个值,那么JVM就会使用这个值来初始化TLAB的大小。但是这样设置不够优雅,其实TLABSize默认值是0,也就是说JVM会推断这个值多大更合适。采用的参数为TLABWasteTargetPercent,用于设置TLAB可占用的Eden空间的百分比,默认值1%,推断方式为TLABSize=Eden×2×1%/线程个数(乘以2是因为假设其内存使用服从均匀分布)

在Java对象分配时,我们总希望它位于TLAB中,如果TLAB满了之后,如何处理呢?前面提到TLAB其实就是Eden的一块区域,在G1中就是HeapRegion的一块空闲区域。所以TLAB满了之后无须做额外的处理,直接保留这一部分空间,重新在Eden/堆分区中分配一块空间给TLAB,然后再在TLAB分配具体的对象。但这里会有两个小问题。

1.如何判断TLAB满了?

按照前面的例子TLAB是1M,当我们使用800K,还是900K,还是950K时被认为满了?问题的答案是如何寻找最大的可能分配对象和减少内存碎片的平衡

2.如何调整TLAB
如果要分配的内存大于TLAB剩余的空间则直接在Eden/HeapRegion中分配。那么这个1/64是否合适?会不会太小,比如通常分配的对象大多是20K,最后剩下16K,这样导致每次都进入Eden/堆分区慢速分配中。所以,JVM还提供了一个参数TLAB WasteIncrement(默认值为4个字)用于动态增加这个refill_waste的值。默认情况下,TLAB大小和refill_waste都会在运行时不断调整,使系统的运行状态达到最优。在动态调整的过程中,也不能无限制变更,所以JVM提供MinTLABSize(默认值2K)用于控制最小值,对于G1来说,由于大对象都不在新生代分区,所以TLAB也不能分配大对象,HeapRegion/2就会被认定为大对象,所以TLAB肯定不会超过HeapRegionSize的一半。

TLAB中的慢速分配

主要的步骤有:

·TLAB的剩余空间是否太小,如果很小,即说明这个空间通常不满足对象的分配,所以最好丢弃,丢弃的方法就是填充一个dummy对象,然后申请新的TLAB来分配对象。

·如果不能丢弃,说明TLAB剩余空间并不小,能满足很多对象的分配,所以不能丢弃这个TLAB,否则内存浪费很多,此时可以把对象分配到堆中,不使用TLAB分配,所以可以直接返回。

为什么要对老的TLAB做清理动作?

TLAB存储的都是已经分配的对象,为什么要清理以及清理什么?其实这里的清理就是把尚未分配的空间分配一个对象(通常是一个int[]),那么为什么要分配一个垃圾对象?代码说明是为了栈解析(Heap Parsable),Heap Parsable是什么?为什么需要设置?

内存管理器(GC)在进行某些需要线性扫描堆里对象的操作时,比如,查看HeapRegion对象、并行标记等,需要知道堆里哪些地方有对象,而哪些地方是空白。对于对象,扫描之后可以直接跳过对象的长度,对于空白的地方只能一个字一个字地扫描,这会非常慢。所以可以把这块空白的地方也分配一个dummy对象(哑元对象),这样GC在线性遍历时就能做到快速遍历了。这样的话就能统一处理

如何申请一个新的TLAB缓冲区

TLAB缓冲区内存的分配也分为两步.

它最终会调用到G1CollectedHeap中分配,其分配主要是在attempt_allocation完成的,步骤也分为两步:快速无锁分配 和 慢速分配

快速无锁分配:
指的是在当前可以分配的堆分区中使用CAS来获取一块内存,如果成功则可以作为TLAB的空间。因为使用CAS可以并行分配,当然也有可能不成功。对于不成功则进行慢速分配

对于不成功则进行慢速分配,慢速分配需要尝试对Heap加锁,扩展新生代区域 或 垃圾回收等处理后再分配

·首先尝试对堆分区进行加锁分配,成功则返回,在attempt_allocation_locked完成。

·不成功,则判定是否可以对新生代分区进行扩展,如果可以扩展则扩展后再分配TLAB,成功则返回,在attempt_allocation_force完成。

·不成功,判定是否可以进行垃圾回收,如果可以进行垃圾回收后再分配,成功则返回,在do_collection_pause完成。

·不成功,如果尝试分配次数达到阈值(默认值是2次)则返回失败。

·如果还可以继续尝试,再次判定是否进行快速分配,如果成功则返回。

·不成功重新再尝试一次,直到成功或者达到阈值失败。

所以慢速分配要么成功分配,要么尝试次数达到阈值后结束并返回NULL。

garbage-first heap   total 131072K, used 37569K [0x00000000f8000000, 0x00000000f8100400, 0x0000000100000000) region size 1024K, 24 young (24576K), 0 survivors (0K)
TLAB: gc thread: 0x0000000059ade800 [id: 16540] desired_size: 491KB slow 
  allocs: 8  refill waste: 7864B alloc: 0.99999    24576KB refills: 50 
  waste  0.0% gc: 0B slow: 816B fast: 0Bd

对于多线程的情况,这里还会有每个线程的输出结果以及一个总结信息。由于篇幅的关系此处都已经省略。下面我们分析日志中TLAB这个信息的每一个字段含义:

·desired_size为期望分配的TLAB的大小,这个值就是我们前面提到如何计算TLABSize的方式。在这个例子中,第一次的时候,不知道会有多少线程,所以初始化为1,desired_size=24576/50=491.5KB这个值是经过取整的。

·slow allocs为发生慢速分配的次数,日志中显示有8次分配到heap而没有使用TLAB。

·refill waste为retire一个TLAB的阈值。

·alloc为该线程在堆分区分配的比例。

·refills发生的次数,这里是50,表示从上一次GC到这次GC期间,一共retire过50个TLAB块,在每一个TLAB块retire的时候都会做一次refill把尚未使用的内存填充为dummy对象。

·waste由3个部分组成:

·gc:发生GC时还没有使用的TLAB的空间。

·slow:产生新的TLAB时,旧的TLAB浪费的空间,这里就是新生成50个TLAB,浪费了816个字节。

·fast:指的是在C1中,发生TLAB retire(产生新的TLAB)时,旧的TLAB浪费的空间。

慢速分配

当不能进行快速分配,就进入到慢速分配。

实际上在TLAB中也有可能进入到慢速分配

这里的慢速分配是指在TLAB中经过努力分配还不能成功,再次进入慢速分配,我们来看一下这个更慢的慢速分配

·attempt_allocation尝试进行对象分配,如果成功则返回。值得注意的是在attempt_allocation里面可能会进行垃圾回收,这里的垃圾回收是指增量的垃圾回收,主要是新生代或者混合收集

·如果大对象在attempt_allocation_humongous,直接分配的老生代。

·如果分配不成功,则进行GC垃圾回收,注意这里的回收主要是Full GC,然后再分配。因为这里是分配的最后一步,所以进行几次不同的垃圾回收和尝试。

·最终成功分配或者失败达到一定次数,则分配失败。

大对象分配

大对象分配和TLAB中的慢速分配基本类似。唯一的区别就是对象大小不同。步骤主要:

·尝试垃圾回收,这里主要是增量回收,同时启动并发标记。

·尝试开始分配对象,对于大对象分为两类,一类是大于HeapRegionSize的一半,但是小于HeapRegionSize,即一个完整的堆分区可以保存,则直接从空闲列表直接拿一个堆分区,或者分配一个新的堆分区。如果是连续对象,则需要多个堆分区,思路同上,但是处理的时候需要加锁。

·如果失败再次尝试垃圾回收,之后再分配。

·最终成功分配或者失败达到一定次数,则分配失败。

最后的分配尝试

先尝试分配一下,因为并发之后可能可以分配:

·尝试扩展新的分区,成功则返回。

·不成功进行Full GC,但是不回收软引用,再次分配成功则返回。

·不成功进行Full GC,回收软引用,最后一次分配成功则返回;不成功返回NULL,即分配失败。

G1垃圾回收的时机

通常来说,在分配对象时如果内存不足,就会触发垃圾回收,G1提供了3种垃圾回收的算法,分别是新生代回收、混合回收和Full GC,所以在内存分配的地方可以看到这3种收集算法。

总结来看,回收发生在两个时机:第一,在分配内存时发现内存不足,进入垃圾回收;第二,外部显式地调用回收的方法,如在Java代码中调用system.gc()进入回收。不同的回收时机选择的回收方式也不同。

分配时发生回收

前面提到快速分配和慢速分配在内存不足时,都有可能发生垃圾回收,回收之后再继续分配。

外部调用的回收

常见有两种外部调用情况可以激活垃圾回收:

·外部显式调用system.gc触发。一般来说,如果我们没有设置DisableExplicitGC(默认为false),表示可以接受这个函数显式地触发GC。这个时候触发的GC都是Full GC,但是如果设置了ExplicitGCInvokesConcurrent,则表示可以进行并发的混合回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FlyingZCC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值