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

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

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


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


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


3.2.2. 重新计算 TLAB 大小

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp_desired_size是什么时候变得呢?怎么变得呢?

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

那是什么时候调用 resize 的呢?一般是每次** GC 完成的时候**。大部分的 GC 都是在gc_epilogue方法里面调用,将每个线程的 TLAB 均 resize 掉。


4. TLAB 回收

TLAB 回收就是指线程将当前的 TLAB 丢弃回 Eden 区。TLAB 回收有两个时机:一个是之前提到的在分配对象时,剩余 TLAB 空间不足,在 TLAB 满但是浪费空间小于最大浪费空间的情况下,回收当前的 TLAB 并获取一个新的。另一个就是在发生 GC 时,其实更准确的说是在 GC 开始扫描时。不同的 GC 可能实现不一样,但是时机是基本一样的,这里以 G1 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);
  }
  //省略其他代码
}

为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?

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 其实就是将 ThreadLocalAllocBuffer 的堆内存指针 MarkWord 置为 NULL
        thread->tlab().retire(&stats);
      } else {
        //当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析
        thread->tlab().make_parsable();
      }
    }
  }
  stats.publish();
}


TLAB 主要流程总结


微信图片_20220624205740.jpg


微信图片_20220624205743.jpg


微信图片_20220624205746.jpg


微信图片_20220624205750.jpg


JFR 对于 TLAB 的监控


根据上面的原理以及源代码分析,可以得知 TLAB 是 Eden 区的一部分,主要用于线程本地的对象分配。在 TLAB 满的时候分配对象内存,可能会发生两种处理:

  1. 线程获取新的 TLAB。老的 TLAB 回归 Eden,Eden进行管理,之后线程通过新的 TLAB 分配对象。
  2. 对象在 TLAB 外分配,也就 Eden 区。

对于 线程获取新的 TLAB 这种处理,也就是 refill,按照 TLAB 设计原理,这个是经常会发生的,每个 epoch 内可能会都会发生几次。但是对象直接在 Eden 区分配,是我们要避免的。JFR 对于

JFR 针对这两种处理有不同的事件可以监控。分别是jdk.ObjectAllocationOutsideTLABjdk.ObjectAllocationInNewTLABjdk.ObjectAllocationInNewTLAB对应 refill,这个一般我们没有监控的必要(在你没有修改默认的 TLAB 参数的前提下),用这个测试并学习 TLAB 的意义比监控的意义更大。jdk.ObjectAllocationOutsideTLAB对应对象直接在 Eden 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。

同时

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
37 2
|
21天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
60 0
|
3天前
|
存储 XML 监控
JVM工作原理与实战(三):字节码文件的组成
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了字节码文件的基础信息、常量池、方法、字段、属性等内容。
|
1月前
|
存储 SQL 关系型数据库
[MySQL]事务原理之redo log,undo log
[MySQL]事务原理之redo log,undo log
|
1月前
|
算法 Oracle Java
【JVM】了解JVM中动态判断对象年龄的原理
【JVM】了解JVM中动态判断对象年龄的原理
24 0
|
2月前
|
算法 Java
深入理解JVM - 解读GC日志
深入理解JVM - 解读GC日志
50 0
|
3月前
|
Java Spring
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
Spring5深入浅出篇:Spring工厂简单原理以及日志应用
|
3月前
|
运维 监控 Java
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南
学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。
54 5
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南
|
3月前
|
存储 缓存 Java
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你认识和了解JMM并发模型的基本原理
每位Java开发者都了解到Java字节码是在Java运行时环境(JRE)上执行的。JRE包含了最为关键的组成部分:Java虚拟机(JVM),它负责分析和执行Java字节码。通常情况下,大多数Java开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对JVM有一定了解的话,将更有助于深入理解Java语言,并解决一些看似困难的问题。
60 4
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你认识和了解JMM并发模型的基本原理
|
3月前
|
监控 Shell 测试技术
Appium日志分析总结Appium工作原理
Appium日志分析总结Appium工作原理
31 0