深刻学习 G1回收器和JVM: G1的对象分配(3)

对象分配概述

  • G1提供了两种分配策略java

    • 基于线程本地分配缓冲区(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相似,惟一区别就是对象大小不一样)

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

    • 若是大于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的调优,其余的垃圾回收器一样适用。
相关文章
相关标签/搜索