通过 JFR 与日志深入探索 JVM - TLAB 原理详解(中)

简介: 通过 JFR 与日志深入探索 JVM - TLAB 原理详解(中)

TLAB 相关源码详解


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;                    //每个 epoch 周期内期望的 refill 次数
//以下是 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 占用所有TLAB最大空间(一般是Eden大小)的期望比例,通过 EMA 算法采集预测
//以下是我们这里不用太关心的 field
HeapWord* _allocation_end;                    // TLAB 真正可以用来分配内存的结束地址,这个是 _end 结束地址排除保留空间,至于为何需要保留空间我们这里先不用关心,稍后我们会解释这个参数
HeapWord* _pf_top;                            // Allocation Prefetch CPU 缓存优化机制相关需要的参数,这里先不用考虑
size_t    _allocated_before_last_gc;          // GC统计数据采集相关,例如线程内存申请数据统计等等,这里先不用关心
unsigned  _number_of_refills;                 // 线程分配内存数据采集相关,TLAB 剩余空间不足分配次数
unsigned  _fast_refill_waste;                 // 线程分配内存数据采集相关,TLAB 快速分配浪费,什么是快速分配,待会会说到
unsigned  _slow_refill_waste;                 // 线程分配内存数据采集相关,TLAB 慢速分配浪费,什么是慢速分配,待会会说到
unsigned  _gc_waste;                          // 线程分配内存数据采集相关,gc浪费
unsigned  _slow_allocations;                  // 线程分配内存数据采集相关,TLAB 慢速分配计数 
size_t    _allocated_size;                    //分配的内存大小
size_t    _bytes_since_last_sample_point;     // JVM TI 采集指标相关 field,这里不用关心


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();
}


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(); 
}


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 的空区域有关。这个原因之前已经说明了。


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);
}


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
40 2
|
27天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
78 0
|
10天前
|
监控 前端开发 安全
JVM工作原理与实战(十四):JDK9及之后的类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JDK8及之前的类加载器、JDK9及之后的类加载器等内容。
19 2
|
10天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
14 2
|
10天前
|
存储 XML 监控
JVM工作原理与实战(三):字节码文件的组成
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了字节码文件的基础信息、常量池、方法、字段、属性等内容。
26 6
|
1月前
|
存储 SQL 关系型数据库
[MySQL]事务原理之redo log,undo log
[MySQL]事务原理之redo log,undo log
|
1月前
|
算法 Oracle Java
【JVM】了解JVM中动态判断对象年龄的原理
【JVM】了解JVM中动态判断对象年龄的原理
26 0
|
2月前
|
算法 Java
深入理解JVM - 解读GC日志
深入理解JVM - 解读GC日志
52 0
|
3月前
|
Java Spring
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
|
3月前
|
运维 监控 Java
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南
学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。
55 5
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南