对象分配概述

  • G1提供了两种分配策略

    • 基于线程本地分配缓冲区(Thread Local Allocation Buffer,TLAB)的快速分配
    • 基于TLAB的慢速分配
    • 慢速分配
  • 当不能成功分配对象时就会触发垃圾回收

image.png

对象分配流程图

线程本地分配缓冲区(Thread Local Allocation Buffer,TLAB)

快速分配

  • TLAB的产生就是为了快速分配内存
  • JVM堆是所有线程的共享区域,所以,从JVM堆空间分配对象时,必须锁定整个堆,以便不会被其他线程中断和影响。
  • TLAB就是为每个线程都分配的一个独立的缓冲区,这样就可以减少使用锁的频率,只有在为每个线程分配TLAB的时候,才需要锁定整个JVM堆
  • TLAB属于Eden区域中的内存,不同线程的TLAB都位于Eden区,Eden区对所有的线程都是可见的。每个线程的TLAB有内存区间,在分配的时候只在这个区间分配。
  • JVM分配TLAB的时候用CAS分配
  • JVM快速分配TLAB对象流程

    1. 从线程的TLAB分配空间,如果成功则返回。
    2. 如果分配失败,则尝试先分配一个新的TLAB,再分配对象
  • 如果TLAB过小,那么TLAB不能储存更多的对象,可能需要不断地给整个堆上锁重新分配新的TLAB。
  • 如果TLAB过大,那么容易导致更多的内存碎片,内存使用效率不高。更容易发生GC。
  • JVM提供了参数 TLABSize来控制TLAB的大小,默认为0,JVM会自动推断这个值多大合适。
  • 参数TLABWasteTargetPercent,用于设置TLAB占Eden空间的百分比默认值1%

    • 推断方式:TLABSize = Eden 2 1% / 线程个数(乘以2是假设内存使用服从均匀分布)

指针碰撞法分配

  1. 如果TLAB剩余空间(end - top) 大于当前对象待分配空间。则直接修改 top = top + objSize(对象大小);
  2. 如果TLAB满了,则会保留这一部分空间,重新从堆内存中划一片空间给TLAB

如何判断TLAB满了

  • 虚拟机内部会维护一个 refill_waste,当请求对象大于refill_waste,会选择在堆中分配,若小于该值,废弃当前的TLAB,新建一个TLAB来分配该对象。
  • refill_waste 可以使用 TLABRefillWasteFraction 参数来调整,默认值为64,即表示使用1/64的TLAB空间作为refill_waste
  • 假设 TLAB为1M,那 refill_waste 为16k,即当TLAB使用了1008k时(1024k - 16k),就直接分配一个新的,否则就尽量使用这个老的TLAB

如何调整TLAB

  • 除了 TLABRefillWasteFraction ,JVM还提供了参数TLAB WasteIncrement(默认值为4个字),用于动态增加refill_waste
  • refill_waste和TLAB的大小都会不断动态调整,使系统状态达到最优。
  • 由于大对象都不会在新生代中,TLAB都不能分配大对象,所以TLAB的大小不会大于HRSize/2。(大于HRSize/2就会被认为是大对象)

TLAB中的慢速分配

  • 如果TLAB中的剩余空间很小(TLAB满了),说明这个空间通常不满足对象分配,可以直接丢弃,填充一个dummy对象,然后申请一个新的TLAB来分配对象。
  • 如果TLAB剩余空间比较多,那就不能丢弃TLAB,这时候就直接将对象分配到堆中,不使用TLAB,直接返回。

清理老的TLAB(dummy对象填充)

  • GC在线性扫描堆的时候(比如查看HeapRegion对象,并行标记等),要知道哪里有对象,哪里是空白的。对于对象,扫描之后,可以直接跳过这个对象的长度,如果是空白的,就需要一个字一个字的扫,效率很低。所以把TLAB的空白地方分配一个dummy对象(哑元对象),这样GC就能做到快速遍历了。

申请一个新的TLAB缓冲区

调用G1CollectHeap中分配,主要是在attempt_allocation中完成
  • 快速无锁分配:在当前可以分配的堆空间内,通过CAS来获取一块内存,如果成功,就可以作为TLAB的空间。
  • 因为使用CAS,所以也有可能不成功。
  • 不成功则进行慢速分配。
  • 慢速分配需要尝试对Heap加锁,拓展新生代区域或垃圾回收等处理后再分配。

    1. 首先尝试对堆分区进行加锁后进行分配,成功则返回。
    2. 如果不成功,则判定是否可以对新生代分区进行拓展。如果可以拓展,则拓展以后再分配TLAB,成功则返回。
    3. 如果不成功,则判定是否可以进行垃圾回收,如果可以进行,垃圾回收以后再分配,成功则返回。
    4. 如果不成功,则再次尝试,如果尝试次数达到阈值(默认2),则返回失败(NULL)。(如果可以继续尝试,则从快速分配开始重头尝试)

日志及解读

通过命令设置参数,如下所示:

-Xmx128M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 
-XX:+PrintTLAB -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest

可以得到:

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 ref?ill waste: 7864B alloc: 0.99999 24576KB ref?ills: 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的时候都会做一次ref?ill把尚未使用的内存填充为dummy对象。
  • waste:由3个部分组成:

    • gc:发生GC时还没有使用的TLAB的空间。
    • slow:产生新的TLAB时,旧的TLAB浪费的空间,这里就是新生成50个TLAB,浪费了816个字节。
    • fast:指的是在C1中,发生TLAB retire(产生新的TLAB)时,旧的TLAB浪费的空间。

慢速分配

这里的慢速分配指的是在TLAB中分配,不能成功,最后进入慢速分配。(比TLAB慢速分配更慢
  • attempt_allocation尝试进行对象分配,如果成功则返回。
  • 如果大对象,在attempt_allocatin_humongous分配,直接分配老年代.
  • 如果分配不成功,则进行GC垃圾回收(主要是FullGC),然后再分配。
  • 最终成功,或者尝试N次后失败,则分配失败。

大对象分配流程(和TLAB类似,唯一区别就是对象大小不同)

  • 尝试垃圾回收(主要是增量回收,同时启动并发标记)
  • 尝试开始分配对象

    • 如果大于HRSize的一半且小于HRSize(即一个完整分区可以保存):直接从空闲列表获得一个分区,或者分配一个新分区。
    • 如果是大于HRSize,则是个连续对象,需要多个分区,思路同上,但是需要加锁。
  • 如果失败再从尝试垃圾回收开始。如果失败达到一定次数,则分配失败。

最后的分配尝试

  • 尝试拓展新的分区,成功则返回
  • 不成功进行则进行FullGC,但是不回收软引用,再次分配成功则返回
  • 不成功再次进行FullGC,回收软引用,成功则返回
  • 不成功则返回Null,分配失败

G1垃圾回收的时机

  • 分配内存时发现内存不足
  • 外部显式调用

    • java代码中的system.gc()

      • 如果设置了DisableExplicitGC(默认为false),则不接受这个函数显式触发GC
      • 默认为FullGC,如果设置了ExplicitGCInvokesConcurrent,表示可以进行并发的混合回收
    • 和JNI交互,JNI代码进入了临界区

      • 如JNI代码为了优化性能,提供了一个函数jni_GetPrimitiveArrayCritical/jni_GetStringCritical用于直接访问原始内存数据,但是为了保证安全必须使用GCLocker进行加锁。当加锁后发生了GC请求,此时GC会被延迟,直到GCLocker执行了unlock会重新补一个GC
      • 如果设置了ExplicitGCInvokesConcurrent,表示可以进行并发的混合回收,如果没有设置,可能启动新生代回收

参数介绍和调优

  • 在优化调试TLAB的时候,在调试环境中可以通过打开PrintTLAB来观察TLAB分配和使用的情况。
  • 参数UseTLAB,指是否使用TLAB。大量的实验可以证明使用TLAB能够加速对象分配;该参数默认是打开的,不要关闭它
  • 参数ResizeTLAB,指是否允许TLAB大小动态调整。前面提到TLAB会进行动态化调整,主要是基于历史信息(分配大小、线程数等),有基准测试表明使用动态调整TLAB大小效率更高。
  • 参数MinTLABSize,指设置TLAB的最小值实际应用需要设置该值,比如64K,一般可以根据情况设置和调整该值。
  • 参数TLABSize,指设置TLAB的大小实际中不要设置TLABSize,设置之后TLAB就不能动态调整了,即会使用一个固定大小的TLAB,前面我们提到GC可以根据情况动态调整TLAB,在分配效率和内存碎片之间找到一个平衡点,如果设置该值则这种平衡就失效了。
  • 参数TLABWasteTargetPercent,指的是TLAB可占用的Eden空间的百分比,默认值是1。可以根据情况调整TLABWasteTargetPercent,增大则可以分配更多的TLAB,3.1节中给出了具体的计算方式;另外如果实际中线程数目很多,建议增大该值,这样每个线程的TLAB不至于太小。
  • 参数TLABRefillWasteFraction,指的是TLAB中浪费空间和TLAB块的比例,默认值是64。可以根据情况调整TLABRef?illWasteFraction,主要考量点是内存碎片和分配效率的平衡,如果发现日志waste中的slow和fast很大,说明浪费严重,可以适当减少该参数值
  • 参数TLABWasteIncrement,指的是动态的增加浪费空间的字节数,默认值是4。增加该值会增加TLAB浪费的空间;一般不用设置
  • 参数GCLockerRetryAllocationCount默认值为2,表示当分配中的垃圾回收次数超过这个阈值之后则直接失败。
TLAB不是G1才引入的,对象分配是JVM提供的基础分配功能,只不过G1结合自己内存分区的特征,以及垃圾回收的具体实现,重新实现了分配的策略,重用了这些参数的功能和使用方法,且没有引入额外的参数,所以这一部分内容不仅适用于G1的调优,其他的垃圾回收器同样适用。

伟大的卷发
4 声望4 粉丝

日拱一卒,以求寸进