精华内容
下载资源
问答
  • tlab

    2020-03-18 13:24:54
    TLAB JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合...

    TLAB
    JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
    也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
    关于对象分配的JDK源码可以参见JVM 之 Java对象创建[初始化]中对OpenJDK源码的分析。
    3. Java对象分配的过程
    编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.
    如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3.
    重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
    在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5.
    执行一次Young GC(minor collection)。
    经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
    对象不在堆上分配主要的原因还是堆是共享的,在堆上分配有锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。
    TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 
    由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。 
    TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。 
    由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。 
    -XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

    对象内存分配的两种方法
    为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    指针碰撞(Serial、ParNew等带Compact过程的收集器) 
    假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。 
    空闲列表(CMS这种基于Mark-Sweep算法的收集器) 
    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。 
    ————————————————
    版权声明:本文为CSDN博主「ZhaoYingChao88」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/zyc88888/article/details/80361635

    展开全文
  • TLAB

    2020-12-17 10:26:00
    为什么会有TLAB(Thread Local Allocation Buffer)? • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;• 由于对象实例的创建在JVM中十分频繁,一次在并发环境下从堆区中划分内存空间是线程不安全的;...

    为什么会有TLAB(Thread Local Allocation Buffer)?

    堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据; 由于对象实例的创建在JVM中十分频繁,一次在并发环境下从堆区中划分内存空间是线程不安全的; 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

    什么是TLAB?

    从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区域内。 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以讲这种内存分配方式成为快速分配策略

    TLAB的再说明:

    尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。 一旦对象在LAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。


    注意:这里是先不算栈上分配和大对象的,因为大对象会直接进去老年代,栈上分配会在TLAB前面
    展开全文
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...

    今天,又是干货满满的一天。这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版

    9. OpenJDK HotSpot TLAB 相关源代码分析

    如果这里看的比较吃力,可以直接看第 10 章,热门 Q&A,里面有很多大家常问的问题

    9.1. TLAB 类构成

    线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB。

    TLAB 包括如下几个 field (HeapWord* 可以理解为堆中的内存地址):
    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //静态全局变量
    static size_t   _max_size;                          // 所有 TLAB 的最大大小
      static int      _reserve_for_allocation_prefetch;   // CPU 缓存优化 Allocation Prefetch 的保留空间,这里先不用关心
      static unsigned _target_refills;                    //每个 GC 周期内期望的重填次数
    
    //以下是 TLAB 的主要构成 field
    HeapWord* _start;                              // TLAB 起始地址,表示堆内存地址都用 HeapWord* 
    HeapWord* _top;                                // 上次分配的内存地址
    HeapWord* _end;                                // TLAB 结束地址
    size_t    _desired_size;                       // TLAB 大小 包括保留空间,表示内存大小都需要通过 size_t 类型,也就是实际字节数除以 HeapWordSize 的值
    size_t    _refill_waste_limit;                 // TLAB最大浪费空间,剩余空间不足分配浪费空间限制。在TLAB剩余空间不足的时候,根据这个值决定分配策略,如果浪费空间大于这个值则直接在 Eden 区分配,如果小于这个值则将当前 TLAB 放回 Eden 区管理并从 Eden 申请新的 TLAB 进行分配。 
    AdaptiveWeightedAverage _allocation_fraction;  // 当前 TLAB 分配比例 EMA
    
    //以下是我们这里不用太关心的 field
    HeapWord* _allocation_end;                    // TLAB 真正可以用来分配内存的结束地址,这个是 _end 结束地址排除保留空间(预留给 dummy object 的对象头空间)
    HeapWord* _pf_top;                            // Allocation Prefetch CPU 缓存优化机制相关需要的参数,这里先不用考虑
    size_t    _allocated_before_last_gc;          // 这个用于计算 图10 中的线程本轮 GC 分配空间的大小,记录上次 GC 时,线程分配的空间大小
    unsigned  _number_of_refills;                 // 线程分配内存数据采集相关,TLAB 剩余空间不足分配次数
    unsigned  _fast_refill_waste;                 // 线程分配内存数据采集相关,TLAB 快速分配浪费,快速分配就是直接在 TLAB 分配,这个在现在 JVM 中已经用不到了
    unsigned  _slow_refill_waste;                 // 线程分配内存数据采集相关,TLAB 慢速分配浪费,慢速分配就是重填一个 TLAB 分配
    unsigned  _gc_waste;                          // 线程分配内存数据采集相关,gc浪费
    unsigned  _slow_allocations;                  // 线程分配内存数据采集相关,TLAB 慢速分配计数 
    size_t    _allocated_size;                    // 分配的内存大小
    size_t    _bytes_since_last_sample_point;     // JVM TI 采集指标相关 field,这里不用关心
    
    

    9.2. TLAB 初始化

    首先是 JVM 启动的时候,全局 TLAB 需要初始化:
    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::startup_initialization() {
      //初始化,也就是归零统计数据
      ThreadLocalAllocStats::initialize();
    
      // 假设平均下来,GC 扫描的时候,每个线程当前的 TLAB 都有一半的内存被浪费,这个每个线程使用内存的浪费的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意,仅最新的那个 TLAB 有浪费,之前 refill 退回的假设是没有浪费的):1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100
      //那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent, 默认也就是 50 次。
      _target_refills = 100 / (2 * TLABWasteTargetPercent);
      // 但是初始的 _target_refills 需要设置最多不超过 2 次来减少 VM 初始化时候 GC 的可能性
      _target_refills = MAX2(_target_refills, 2U);
    
    //如果 C2 JIT 编译存在并启用,则保留 CPU 缓存优化 Allocation Prefetch 空间,这个这里先不用关心,会在别的章节讲述
    #ifdef COMPILER2
      if (is_server_compilation_mode_vm()) {
        int lines =  MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;
        _reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /
                                           (int)HeapWordSize;
      }
    #endif
    
      // 初始化 main 线程的 TLAB
      guarantee(Thread::current()->is_Java_thread(), "tlab initialization thread not Java thread");
      Thread::current()->tlab().initialize();
      log_develop_trace(gc, tlab)("TLAB min: " SIZE_FORMAT " initial: " SIZE_FORMAT " max: " SIZE_FORMAT,
                                   min_size(), Thread::current()->tlab().initial_desired_size(), max_size());
    }
    

    每个线程维护自己的 TLAB,同时每个线程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,线程数量,还有线程的对象分配速率决定。
    在 Java 线程开始运行时,会先分配 TLAB:
    src/hotspot/share/runtime/thread.cpp

    void JavaThread::run() {
      // initialize thread-local alloc buffer related fields
      this->initialize_tlab();
      //剩余代码忽略
    }
    

    分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。
    src/hotspot/share/runtime/thread.hpp

    void initialize_tlab() {
        //如果没有通过 -XX:-UseTLAB 禁用 TLAB,则初始化TLAB
        if (UseTLAB) {
          tlab().initialize();
        }
    }
    
    // Thread-Local Allocation Buffer (TLAB) support
    ThreadLocalAllocBuffer& tlab()                 {
      return _tlab; 
    }
    
    ThreadLocalAllocBuffer _tlab;
    

    ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我们要关心的各种 field:
    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::initialize() {
      //设置初始指针,由于还没有从 Eden 分配内存,所以这里都设置为 NULL
      initialize(NULL,                    // start
                 NULL,                    // top
                 NULL);                   // end
      //计算初始期望大小,并设置
      set_desired_size(initial_desired_size());
      //所有 TLAB 总大小,不同的 GC 实现有不同的 TLAB 容量, 一般是 Eden 区大小
      //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
      size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;
      //计算这个线程的 TLAB 期望占用所有 TLAB 总体大小比例
      //TLAB 期望占用大小也就是这个 TLAB 大小乘以期望 refill 的次数
      float alloc_frac = desired_size() * target_refills() / (float) capacity;
      //记录下来,用于计算 EMA
      _allocation_fraction.sample(alloc_frac);
      //计算初始 refill 最大浪费空间,并设置
      //如前面原理部分所述,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction
      set_refill_waste_limit(initial_refill_waste_limit());
      //重置统计
      reset_statistics();
    }
    

    9.2.1. 初始期望大小是如何计算的呢?

    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //计算初始大小
    size_t ThreadLocalAllocBuffer::initial_desired_size() {
      size_t init_sz = 0;
      //如果通过 -XX:TLABSize 设置了 TLAB 大小,则用这个值作为初始期望大小
      //表示堆内存占用大小都需要用占用几个 HeapWord 表示,所以用TLABSize / HeapWordSize
      if (TLABSize > 0) {
        init_sz = TLABSize / HeapWordSize;
      } else {
        //获取当前epoch内线程数量期望,这个如之前所述通过 EMA 预测
        unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();
        //不同的 GC 实现有不同的 TLAB 容量,Universe::heap()->tlab_capacity(thread()) 一般是 Eden 区大小
        //例如 G1 GC,就是等于 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,可以理解为年轻代减去Survivor区,也就是Eden区
        //整体大小等于 Eden区大小/(当前 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置)
        //target_refills已经在 JVM 初始化所有 TLAB 全局配置的时候初始化好了
        init_sz  = (Universe::heap()->tlab_capacity(thread()) / HeapWordSize) /
                          (nof_threads * target_refills());
        //考虑对象对齐,得出最后的大小
        init_sz = align_object_size(init_sz);
      }
      //保持大小在  min_size() 还有 max_size() 之间
      //min_size主要由 MinTLABSize 决定
      init_sz = MIN2(MAX2(init_sz, min_size()), max_size());
      return init_sz;
    }
    
    //最小大小由 MinTLABSize 决定,需要表示为 HeapWordSize,并且考虑对象对齐,最后的 alignment_reserve 是 dummy object 填充的对象头大小(这里先不考虑 JVM 的 CPU 缓存 prematch,我们会在其他章节详细分析)。
    static size_t min_size()                       { 
        return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve(); 
    }
    

    9.2.2. TLAB 最大大小是怎样决定的呢?

    不同的 GC 方式,有不同的方式:

    G1 GC 中为大对象(humongous object)大小,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    // For G1 TLABs should not contain humongous objects, so the maximum TLAB size
    // must be equal to the humongous object limit.
    size_t G1CollectedHeap::max_tlab_size() const {
      return align_down(_humongous_object_threshold_in_words, MinObjAlignment);
    }
    

    ZGC 中为页大小的 8 分之一,类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度:
    src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp

    MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);
    

    src/hotspot/share/gc/z/zHeap.cpp

    const size_t      ZObjectSizeLimitSmall         = ZPageSizeSmall / 8;
    

    对于其他的 GC,则是 int 数组的最大大小,这个和为了填充 dummy object 表示 TLAB 的空区域有关。这个原因之前已经说明了。

    9.3. TLAB 分配内存

    当 new 一个对象时,需要调用instanceOop InstanceKlass::allocate_instance(TRAPS)
    src/hotspot/share/oops/instanceKlass.cpp

    instanceOop InstanceKlass::allocate_instance(TRAPS) {
      bool has_finalizer_flag = has_finalizer(); // Query before possible GC
      int size = size_helper();  // Query before forming handle.
    
      instanceOop i;
    
      i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
      if (has_finalizer_flag && !RegisterFinalizersAtInit) {
        i = register_finalizer(i, CHECK_NULL);
      }
      return i;
    }
    

    其核心就是heap()->obj_allocate(this, size, CHECK_NULL)从堆上面分配内存:
    src/hotspot/share/gc/shared/collectedHeap.inline.hpp

    inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
      ObjAllocator allocator(klass, size, THREAD);
      return allocator.allocate();
    }
    

    使用全局的 ObjAllocator 实现进行对象内存分配:
    src/hotspot/share/gc/shared/memAllocator.cpp

    oop MemAllocator::allocate() const {
      oop obj = NULL;
      {
        Allocation allocation(*this, &obj);
        //分配堆内存,继续看下面一个方法
        HeapWord* mem = mem_allocate(allocation);
        if (mem != NULL) {
          obj = initialize(mem);
        } else {
          // The unhandled oop detector will poison local variable obj,
          // so reset it to NULL if mem is NULL.
          obj = NULL;
        }
      }
      return obj;
    }
    HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
      //如果使用了 TLAB,则从 TLAB 分配,分配代码继续看下面一个方法
      if (UseTLAB) {
        HeapWord* result = allocate_inside_tlab(allocation);
        if (result != NULL) {
          return result;
        }
      }
      //否则直接从 tlab 外分配
      return allocate_outside_tlab(allocation);
    }
    HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
      assert(UseTLAB, "should use UseTLAB");
    
      //从当前线程的 TLAB 分配内存,TLAB 快分配
      HeapWord* mem = _thread->tlab().allocate(_word_size);
      //如果没有分配失败则返回
      if (mem != NULL) {
        return mem;
      }
    
      //如果分配失败则走 TLAB 慢分配,需要 refill 或者直接从 Eden 分配
      return allocate_inside_tlab_slow(allocation);
    }
    

    9.3.1. TLAB 快分配

    src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp

    inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
      //验证各个内存指针有效,也就是 _top 在 _start 和 _end 范围内
      invariants();
      HeapWord* obj = top();
      //如果空间足够,则分配内存
      if (pointer_delta(end(), obj) >= size) {
        set_top(obj + size);
        invariants();
        return obj;
      }
      return NULL;
    }
    

    9.3.2. TLAB 慢分配

    src/hotspot/share/gc/shared/memAllocator.cpp

    HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const {
      HeapWord* mem = NULL;
      ThreadLocalAllocBuffer& tlab = _thread->tlab();
    
      // 如果 TLAB 剩余空间大于 最大浪费空间,则记录并让最大浪费空间递增
      if (tlab.free() > tlab.refill_waste_limit()) {
        tlab.record_slow_allocation(_word_size);
        return NULL;
      }
    
      //重新计算 TLAB 大小
      size_t new_tlab_size = tlab.compute_size(_word_size);
      //TLAB 放回 Eden 区
      tlab.retire_before_allocation();
      
      if (new_tlab_size == 0) {
        return NULL;
      }
    
      // 计算最小大小
      size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size);
      //分配新的 TLAB 空间,并在里面分配对象
      mem = Universe::heap()->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size);
      if (mem == NULL) {
        assert(allocation._allocated_tlab_size == 0,
               "Allocation failed, but actual size was updated. min: " SIZE_FORMAT
               ", desired: " SIZE_FORMAT ", actual: " SIZE_FORMAT,
               min_tlab_size, new_tlab_size, allocation._allocated_tlab_size);
        return NULL;
      }
      assert(allocation._allocated_tlab_size != 0, "Allocation succeeded but actual size not updated. mem at: "
             PTR_FORMAT " min: " SIZE_FORMAT ", desired: " SIZE_FORMAT,
             p2i(mem), min_tlab_size, new_tlab_size);
      //如果启用了 ZeroTLAB 这个 JVM 参数,则将对象所有字段置零值
      if (ZeroTLAB) {
        // ..and clear it.
        Copy::zero_to_words(mem, allocation._allocated_tlab_size);
      } else {
        // ...and zap just allocated object.
      }
    
      //设置新的 TLAB 空间为当前线程的 TLAB
      tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);
      //返回分配的对象内存地址
      return mem;
    }
    

    9.3.2.1 TLAB最大浪费空间

    TLAB最大浪费空间 _refill_waste_limit 初始值为 TLAB 大小除以 TLABRefillWasteFraction:
    src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp

    size_t initial_refill_waste_limit()            { return desired_size() / TLABRefillWasteFraction; }
    

    每次慢分配,调用record_slow_allocation(size_t obj_size)记录慢分配的同时,增加 TLAB 最大浪费空间的大小:

    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) {
      //每次慢分配,_refill_waste_limit 增加 refill_waste_limit_increment,也就是 TLABWasteIncrement
      set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment());
      _slow_allocations++;
      log_develop_trace(gc, tlab)("TLAB: %s thread: " INTPTR_FORMAT " [id: %2d]"
                                  " obj: " SIZE_FORMAT
                                  " free: " SIZE_FORMAT
                                  " waste: " SIZE_FORMAT,
                                  "slow", p2i(thread()), thread()->osthread()->thread_id(),
                                  obj_size, free(), refill_waste_limit());
    }
    //refill_waste_limit_increment 就是 JVM 参数 TLABWasteIncrement
    static size_t refill_waste_limit_increment()   { return TLABWasteIncrement; }
    

    9.3.2.2. 重新计算 TLAB 大小

    重新计算会取 当前堆剩余给 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 中的小的那个:

    src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp

    inline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) {
      //获取当前堆剩余给 TLAB 可分配的空间
      const size_t available_size = Universe::heap()->unsafe_max_tlab_alloc(thread()) / HeapWordSize;
      //取 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 以及 TLAB 最大大小中的小的那个
      size_t new_tlab_size = MIN3(available_size, desired_size() + align_object_size(obj_size), max_size());
    
      // 确保大小大于 dummy obj 对象头
      if (new_tlab_size < compute_min_size(obj_size)) {
        log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns failure",
                            obj_size);
        return 0;
      }
      log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns " SIZE_FORMAT,
                          obj_size, new_tlab_size);
      return new_tlab_size;
    }
    

    9.3.2.3. 当前 TLAB 放回堆

    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    //在TLAB慢分配被调用,当前 TLAB 放回堆
    void ThreadLocalAllocBuffer::retire_before_allocation() {
      //将当前 TLAB 剩余空间大小加入慢分配浪费空间大小
      _slow_refill_waste += (unsigned int)remaining();
      //执行 TLAB 退还给堆,这个在后面 GC 的时候还会被调用用于将所有的线程的 TLAB 退回堆
      retire();
    }
    
    //对于 TLAB 慢分配,stats 为空
    //对于 GC 的时候调用,stats 用于记录每个线程的数据
    void ThreadLocalAllocBuffer::retire(ThreadLocalAllocStats* stats) {
      
      if (stats != NULL) {
        accumulate_and_reset_statistics(stats);
      }
      //如果当前 TLAB 有效
      if (end() != NULL) {
        invariants();
        //将用了的空间记录如线程分配对象大小记录
        thread()->incr_allocated_bytes(used_bytes());
        //填充dummy object
        insert_filler();
        //清空当前 TLAB 指针
        initialize(NULL, NULL, NULL);
      }
    }
    

    9.4. GC 相关 TLAB 操作

    9.4.1. GC 前

    不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在真正 GC 前:

    src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    void G1CollectedHeap::gc_prologue(bool full) {
      //省略其他代码
    
      // Fill TLAB's and such
      {
        Ticks start = Ticks::now();
        //确保堆内存是可以解析的
        ensure_parsability(true);
        Tickspan dt = Ticks::now() - start;
        phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS);
      }
      //省略其他代码
    }
    
    

    为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?其实主要就是退还每个线程的 TLAB 以及填充 dummy object。

    src/hotspot/share/gc/g1/g1CollectedHeap.cpp

    void CollectedHeap::ensure_parsability(bool retire_tlabs) {
      //真正的 GC 肯定发生在安全点上,这个在后面安全点章节会详细说明
      assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
             "Should only be called at a safepoint or at start-up");
    
      ThreadLocalAllocStats stats;
      for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) {
        BarrierSet::barrier_set()->make_parsable(thread);
        //如果全局启用了 TLAB
        if (UseTLAB) {
          //如果指定要回收,则回收 TLAB
          if (retire_tlabs) {
            //回收 TLAB,调用  9.3.2.3. 当前 TLAB 放回堆 提到的 retire 方法
            thread->tlab().retire(&stats);
          } else {
            //当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析
            thread->tlab().make_parsable();
          }
        }
      }
    
      stats.publish();
    }
    

    9.4.2. GC 后

    不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在 GC 后:

    src/hotspot/share/gc/g1/g1CollectedHeap.cpp
    _desired_size是什么时候变得呢?怎么变得呢?

    void G1CollectedHeap::gc_epilogue(bool full) {
        //省略其他代码
        resize_all_tlabs();
    }
    

    src/hotspot/share/gc/shared/collectedHeap.cpp

    void CollectedHeap::resize_all_tlabs() {
      //需要在安全点,GC 会处于安全点的
      assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(),
             "Should only resize tlabs at safepoint");
      //如果 UseTLAB 和 ResizeTLAB 都是打开的(默认就是打开的)
      if (UseTLAB && ResizeTLAB) {
        for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) {
          //重新计算每个线程 TLAB 期望大小
          thread->tlab().resize();
        }
      }
    }
    

    重新计算每个线程 TLAB 期望大小:
    src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

    void ThreadLocalAllocBuffer::resize() {
      assert(ResizeTLAB, "Should not call this otherwise");
      //根据 _allocation_fraction 这个 EMA 采集得出平均数乘以Eden区大小,得出 TLAB 当前预测占用内存比例
      size_t alloc = (size_t)(_allocation_fraction.average() *
                              (Universe::heap()->tlab_capacity(thread()) / HeapWordSize));
      //除以目标 refill 次数就是新的 TLAB 大小,和初始化时候的计算方法差不多
      size_t new_size = alloc / _target_refills;
      //保证在 min_size 还有 max_size 之间
      new_size = clamp(new_size, min_size(), max_size());
    
      size_t aligned_new_size = align_object_size(new_size);
    
      log_trace(gc, tlab)("TLAB new size: thread: " INTPTR_FORMAT " [id: %2d]"
                          " refills %d  alloc: %8.6f desired_size: " SIZE_FORMAT " -> " SIZE_FORMAT,
                          p2i(thread()), thread()->osthread()->thread_id(),
                          _target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size);
      //设置新的 TLAB 大小
      set_desired_size(aligned_new_size);
      //重置 TLAB 最大浪费空间
      set_refill_waste_limit(initial_refill_waste_limit());
    }
    
    展开全文
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...

    今天,又是干货满满的一天。这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版

    10. TLAB 流程常见问题 Q&A

    这里我会持续更新的,解决大家的各种疑问

    10.1. 为何 TLAB 在退还给堆的时候需要填充 dummy object

    主要保证 GC 的时候扫描高效。由于 TLAB 仅线程内知道哪些被分配了,在 GC 扫描发生时返回 Eden 区,如果不填充的话,外部并不知道哪一部分被使用哪一部分没有,需要做额外的检查,如果填充已经确认会被回收的对象,也就是 dummy object, GC 会直接标记之后跳过这块内存,增加扫描效率。反正这块内存已经属于 TLAB,其他线程在下次扫描结束前是无法使用的。这个 dummy object 就是 int 数组。为了一定能有填充 dummy object 的空间,一般 TLAB 大小都会预留一个 dummy object 的 header 的空间,也是一个 int[] 的 header,所以 TLAB 的大小不能超过int 数组的最大大小,否则无法用 dummy object 填满未使用的空间。

    10.2. 为何 TLAB 需要最大浪费空间限制

    当重新分配一个 TLAB 的时候,原有的 TLAB 可能还有空间剩余。原有的 TLAB 被退回堆之前,需要填充好 dummy object。这样导致这块内存无法分配对象,所示被称为“浪费”。如果不限制,遇到 TLAB 剩余空间不足的情况就会重新申请,导致分配效率降低,大部分空间被 dummy object 占满了,导致 GC 更加频繁。

    10.3. 为何 TLAB 重填次数配置 等于 100 / (2 * TLABWasteTargetPercent)

    TLABWasteTargetPercent 描述了初始最大浪费空间配置占 TLAB 的比例

    首先,最理想的情况就是尽量让所有对象在 TLAB 内分配,也就是 TLAB 可能要占满 Eden。
    在下次 GC 扫描前,退回 Eden 的内存别的线程是不能用的,因为剩余空间已经填满了 dummy object。所以所有线程使用内存大小就是 下个 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置,对象一般都在 Eden 区由某个线程分配,也就所有线程使用内存大小就最好是整个 Eden。但是这种情况太过于理想,总会有内存被填充了 dummy object而造成了浪费,因为 GC 扫描随时可能发生。假设平均下来,GC 扫描的时候,每个线程当前的 TLAB 都有一半的内存被浪费,这个每个线程使用内存的浪费的百分比率(也就是 TLABWasteTargetPercent),也就是等于(注意,仅最新的那个 TLAB 有浪费,之前 refill 退回的假设是没有浪费的):

    1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100

    那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent, 默认也就是 50 次。

    10.4. 为何考虑 ZeroTLAB

    当分配出来 TLAB 之后,根据 ZeroTLAB 配置,决定是否将每个字节赋 0。在 TLAB 申请时,由于申请 TLAB 都发生在对象分配的时候,也就是这块内存会立刻被使用,并修改赋值。操作内存,涉及到 CPU 缓存行,如果是多核环境,还会涉及到 CPU 缓存行 false sharing,为了优化,JVM 在这里做了 Allocation Prefetch,简单理解就是分配 TLAB 的时候,会尽量加载这块内存到 CPU 缓存,也就是在分配 TLAB 内存的时候,修改内存是最高效的

    在创建对象的时候,本来也要对每个字段赋初始值,大部分字段初始值都是 0,并且,在 TLAB 返还到堆时,剩余空间填充的也是 int[] 数组,里面都是 0。

    所以,TLAB 刚分配出来的时候,赋 0 避免了后续再赋 0。也能利用好 Allocation prefetch 的机制适应 CPU 缓存行(Allocation prefetch 的机制详情会在另一个系列说明)

    10.5. 为何 JVM 需要预热,为什么 Java 代码越执行越快(这里只提 TLAB 相关的,JIT,MetaSpace,GC等等其他系列会说)

    根据之前的分析,每个线程的 TLAB 的大小,会根据线程分配的特性,不断变化并趋于稳定,大小主要是由分配比例 EMA 决定,但是这个采集是需要一定运行次数的。并且 EMA 的前 100 次采集默认是不够稳定的,所以 TLAB 大小也在程序一开始的时候变化频繁。当程序线程趋于稳定,运行一段时间后, 每个线程 TLAB 大小也会趋于稳定并且调整到最适合这个线程对象分配特性的大小。这样,就更接近最理想的只有 Eden 区满了才会 GC,所有 Eden 区的对象都是通过 TLAB 分配的高效分配情况。这就是 Java 代码越执行越快在 TLAB 方面的原因。

    展开全文
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • 上一篇我们详细的分析了 TLAB 的原理以及生命周期,并且提出 JFR 相关的两个事件:在线程分配对象时,如果 TLAB 不够,则根据最大允许浪费空间,决定是回收当前 TLAB 还是重新获取一个 TLAB 进行分配还是直接在堆上...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • 什么是 TLABTLAB(Thread Local Allocation Buffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。既然是一个内存分配区域,我们就先要搞清楚 Java 内存大概是如何分配的。 我们一般认为 Java 中 new 的...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • JVM-TLAB

    千次阅读 2015-06-25 10:12:39
    TLAB
  • TLAB 全网最硬核的解析,请参考:全网最硬核 JVM TLAB 分析 TLAB 在何时退还给堆? 有两种情况: 当前 TLAB 不足分配,并且剩余空间小于当前线程最大浪费空间限制时。 发生 GC 时,在 GC 扫描前的时候。 什么是 ...
  • 线程TLAB

    2019-12-14 23:46:14
    参考:https://www.jianshu.com/p/8be816cbb5ed ... TLAB 全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 虚拟机默认是开启的:-XX:+UseTLAB, -XX...
  • 了解一下TLAB

    2020-08-20 12:12:38
    TLAB是什么? TLAB的全称是Thread Local Allocation Buffer,翻译过来就是线程本地分配缓存。 首先从Thread Local这两个单词能够联想到一个本地线程变量类ThreadLocal,该类可以用来维护线程私有变量,而TLAB则是一...
  • 这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜) 全网最硬核 JVM TLAB 分析 1. 内存分配...
  • TLAB简单介绍

    千次阅读 2019-05-15 17:04:02
    TLAB(thread-local-allocation-buffer)简单介绍
  • jvm:TLAB

    2018-10-06 09:45:23
    TLAB JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也...
  • JVM区域TLAB

    2019-07-22 09:47:34
    TLAB全称是Thread Local Allocation Buffer即线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的,每一个线程都会产生一个TLAB,该线程独享的工作区域,JAVA虚拟机使用这种TLAB...
  • TLAB线程本地分配缓存区是什么?工作原理分析,TLAB全称Thread Local Allocation Buffer,即线程本地分配缓存区,是一个线程专用的内存分配区域。在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程...
  • jvm中的TLAB

    2020-05-23 16:19:28
    TLAB的来源2.1 逃逸分析2.2 指针碰撞 一. 回顾 前面了解了jvm中的逃逸分析,今天了解一下TLAB(Thread Local Allocation Buffer)。 本文仅供自己参考,如有错误请指正 参考自: 浅析java中的TLAB 二. TLAB的来源 ...
  • JVM中TLAB

    2019-10-24 20:39:55
    TLAB区配置 -XX:+UseTLAB 表示,是否使用TLAB -XX:+TLABSize 表示,设置TLAB大小 -XX:TLABRefillWasteFraction 表示,设置进入TLAB空间,单个对象大小 是一个比例值,默认为64 如果,对象小于整个空间的1/64,则...
  • TLAB 基本原理

    2018-05-23 15:20:26
    转载,其中详细描述了tlab原理,与部分书籍描述相差甚远
  • 当分配一个对象堆内存空间时,在 CollectedHeap 上首先都会检查是否启用了 TLAB,如果启用了,则会尝试 TLAB 分配;如果当前线程的 TLAB 大小足够,那么从线程当前的 TLAB 中分配;如果不够,但是

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,298
精华内容 5,319
关键字:

tlab