Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationSample(下)

简介: Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationSample(下)

输出示例:

//main线程在初始化 JVM 的时候,分配了一些其他对象,所以这里 weight 很大
jdk.ObjectAllocationSample {
  startTime = 10:16:24.677
  //触发本次事件的对象的类
  objectClass = byte[] (classLoader = bootstrap)
  //注意,这个不是对象大小,而是该线程距离上次被采集 jdk.ObjectAllocationSample 事件到这个事件的这段时间,线程分配的对象总大小
  weight = 15.9 MB
  eventThread = "main" (javaThreadId = 1)
  stackTrace = [
    com.github.hashjang.jfr.test.TestObjectAllocationSample.main(String[]) line: 42
  ]
}
jdk.ObjectAllocationSample {
  startTime = 10:16:25.690
  objectClass = byte[] (classLoader = bootstrap)
  weight = 10.0 MB
  eventThread = "main" (javaThreadId = 1)
  stackTrace = [
    com.github.hashjang.jfr.test.TestObjectAllocationSample.main(String[]) line: 42
  ]
}
jdk.ObjectAllocationSample {
  startTime = 10:16:26.702
  objectClass = byte[] (classLoader = bootstrap)
  weight = 1.0 MB
  eventThread = "Thread-0" (javaThreadId = 27)
  stackTrace = [
    com.github.hashjang.jfr.test.TestObjectAllocationSample.lambda$main$0() line: 48
    java.lang.Thread.run() line: 831
  ]
}
jdk.ObjectAllocationSample {
  startTime = 10:16:27.718
  objectClass = byte[] (classLoader = bootstrap)
  weight = 10.0 MB
  eventThread = "Thread-0" (javaThreadId = 27)
  stackTrace = [
    com.github.hashjang.jfr.test.TestObjectAllocationSample.lambda$main$0() line: 48
    java.lang.Thread.run() line: 831
  ]
}

各位读者可以将采集频率改成 "100/s",就能看到基本所有代码里面的对象分配都被采集成为一个事件了。


底层原理与相关 JVM 源码


首先我们来看下 Java 对象分配的流程:


微信图片_20220625131726.jpg


对于 HotSpot JVM 实现,所有的 GC 算法的实现都是一种对于堆内存的管理,也就是都实现了一种堆的抽象,它们都实现了接口 CollectedHeap。当分配一个对象堆内存空间时,在 CollectedHeap 上首先都会检查是否启用了 TLAB,如果启用了,则会尝试 TLAB 分配;如果当前线程的 TLAB 大小足够,那么从线程当前的 TLAB 中分配;如果不够,但是当前 TLAB 剩余空间小于最大浪费空间限制,则从堆上(一般是 Eden 区) 重新申请一个新的 TLAB 进行分配。否则,直接在 TLAB 外进行分配。TLAB 外的分配策略,不同的 GC 算法不同。例如G1:

  • 如果是 Humongous 对象(对象在超过 Region 一半大小的时候),直接在 Humongous 区域分配(老年代的连续区域)。
  • 根据 Mutator 状况在当前分配下标的 Region 内分配

jdk.ObjectAllocationSample 事件只关心 TLAB 外分配以及 TLAB 重分配,因为这也是程序主要需要的优化点。throttle 配置,是限制在一段时间内只能采集这么多的事件。但是我们究竟怎么筛选采集哪些事件呢?假设我们配置的是 100/s,首先想到的是时间窗口,采集这一窗口内开头的 100 个事件。这样显然是不符合我们的要求的,我们并不能保证性能瓶颈的事件就在每秒的前 100 个,并且我们的程序可能每秒发生很多很多次 TLAB 外分配,仅凭前 100 个事件并不能很好的采集我们想看到的事件。所以,JDK 内部通过 EWMA(Exponential Weighted Moving Average)的算法估计何时的采集时间以及越大分配上报次数越多的这样的优化来实现更准确地采样如果是直接在 TLAB 外进行分配,才可能生成 jdk.ObjectAllocationSample 事件

参考源码:

allocTracer.cpp

//在每次发生 TLAB 重分配的时候,调用这个方法上报
void AllocTracer::send_allocation_in_new_tlab(Klass* klass, HeapWord* obj, size_t tlab_size, size_t alloc_size, Thread* thread) {
  JFR_ONLY(JfrAllocationTracer tracer(obj, alloc_size, thread);)
  //立刻生成 jdk.ObjectAllocationInNewTLAB 这个事件
  EventObjectAllocationInNewTLAB event;
  if (event.should_commit()) {
    event.set_objectClass(klass);
    event.set_allocationSize(alloc_size);
    event.set_tlabSize(tlab_size);
    event.commit();
  }
  const int64_t allocated_bytes = load_allocated_bytes(thread);
  if (allocated_bytes == 0) {
    return;
  }
  //采样 jdk.ObjectAllocationSample 事件
  send_allocation_sample(klass, allocated_bytes);
}
//在每次发生 TLAB 外分配的时候,调用这个方法上报
void AllocTracer::send_allocation_outside_tlab(Klass* klass, HeapWord* obj, size_t alloc_size, Thread* thread) {
  JFR_ONLY(JfrAllocationTracer tracer(obj, alloc_size, thread);)
  //立刻生成 jdk.ObjectAllocationOutsideTLAB 这个事件
  EventObjectAllocationOutsideTLAB event;
  if (event.should_commit()) {
    event.set_objectClass(klass);
    event.set_allocationSize(alloc_size);
    event.commit();
  }
  //归一化分配数据并采样 jdk.ObjectAllocationSample 事件
  normalize_as_tlab_and_send_allocation_samples(klass, static_cast<intptr_t>(alloc_size), thread);
}

再来看归一化分配数据并生成 jdk.ObjectAllocationSample 事件的具体内容:

static void normalize_as_tlab_and_send_allocation_samples(Klass* klass, intptr_t obj_alloc_size_bytes, Thread* thread) {
  //读取当前线程分配过的字节大小
  const int64_t allocated_bytes = load_allocated_bytes(thread);
  assert(allocated_bytes > 0, "invariant"); // obj_alloc_size_bytes is already attributed to allocated_bytes at this point.
  //如果没有使用 TLAB,那么不需要处理,allocated_bytes 肯定只包含 TLAB 外分配的字节大小
  if (!UseTLAB) {
    //采样 jdk.ObjectAllocationSample 事件
    send_allocation_sample(klass, allocated_bytes);
    return;
  }
  //获取当前线程的 TLAB 期望大小
  const intptr_t tlab_size_bytes = estimate_tlab_size_bytes(thread);
  //如果当前线程分配过的字节大小与上次读取的当前线程分配过的字节大小相差不超过 TLAB 期望大小,证明可能是由于 TLAB 快满了导致的 TLAB 外分配,并且大小不大,没必要上报。
  if (allocated_bytes - _last_allocated_bytes < tlab_size_bytes) {
    return;
  }
  assert(obj_alloc_size_bytes > 0, "invariant");
  //利用这个循环,如果当前线程分配过的字节大小越大,则采样次数越多,越容易被采集到。
  do {
    if (send_allocation_sample_with_result(klass, allocated_bytes)) {
      return;
    }
    obj_alloc_size_bytes -= tlab_size_bytes;
  } while (obj_alloc_size_bytes > 0);
}

这里我们就观察到了 JDK 做的第一个上报优化算法:如果本次分配对象大小越大,那么这个循环次数就会越多,采样次数就越多,被采集到的概率也越大 接下来来看具体的采样方法:

inline bool send_allocation_sample_with_result(const Klass* klass, int64_t allocated_bytes) {
  assert(allocated_bytes > 0, "invariant");
  EventObjectAllocationSample event;
  //判断事件是否应该 commit,只有 commit 的事件才会被采集
  if (event.should_commit()) {
    //weight 等于上次记录当前线程的 threadLocal 的 allocated_bytes 减去当前线程的 allocated_bytes
    //由于不是每次线程发生 TLAB 外分配的时候上报都会被采集,所以需要记录上次被采集时候的线程分配的 allocated_bytes 大小,计算与当前的差值就是本次上报的事件中的线程距离上次上报分配的对象大小。
    const size_t weight = allocated_bytes - _last_allocated_bytes;
    assert(weight > 0, "invariant");
    //objectClass 即触发上报的分配对象的 class
    event.set_objectClass(klass);
    //weight 并不代表 objectClass 的对象的大小,而是这个线程距离上次上报被采集分配的对象大小
    event.set_weight(weight);
    event.commit();
    //只有事件 commit,也就是被采集,才会更新 _last_allocated_bytes 这个 threadLocal 变量
    _last_allocated_bytes = allocated_bytes;
    return true;
  }
  return false;
}

通过这里的代码我们明白了:

  • ObjectClass 是 TLAB 外分配对象的 class,也是本次触发记录jdk.ObjectAllocationSample 事件的对象的 class
  • weight 是线程距离上次记录 jdk.ObjectAllocationSample 事件到当前这个事件时间内,线程分配的对象大小

这里通常会误以为 weight 就是本次事件 ObjectClass 的对象大小。这个需要着重注意下。

那么如何判断的事件是否应该 commit? 这里走的是 JFR 通用逻辑: jfrEvent.hpp

bool should_commit() {
    if (!_started) {
      return false;
    }
    if (_untimed) {
      return true;
    }
    if (_evaluated) {
      return _should_commit;
    }
    _should_commit = evaluate();
    _evaluated = true;
    return _should_commit;
}
bool evaluate() {
    assert(_started, "invariant");
    if (_start_time == 0) {
      set_starttime(JfrTicks::now());
    } else if (_end_time == 0) {
      set_endtime(JfrTicks::now());
    }
    if (T::isInstant || T::isRequestable) {
      return T::hasThrottle ? JfrEventThrottler::accept(T::eventId, _untimed ? 0 : _start_time) : true;
    }
    if (_end_time - _start_time < JfrEventSetting::threshold(T::eventId)) {
      return false;
    }
    //这里我们先只关心 Throttle
    return T::hasThrottle ? JfrEventThrottler::accept(T::eventId, _untimed ? 0 : _end_time) : true;
}

这里涉及 JfrEventThrottler 控制实现 throttle 配置。主要通过 EWMA 算法实现对于下次合适的采集时间间隔的不断估算优化更新,来采集到最合适的 jdk.ObjectAllocationSample,同时这种算法并不像滑动窗口那样记录历史数据导致占用很大内存,指数移动平均(exponential moving average),或者叫做指数加权移动平均(exponentially weighted moving average),是以指数式递减加权的移动平均,各数值的加权影响力随时间呈指数式递减,可以用来估计变量的局部均值,使得变量的更新与一段时间内的历史取值有关。

假设每次采集数据为 P(n),权重衰减程度为 t,t 在 0~1 之间:


image.png


上面的公式,也可以写作:


image.png


从这个公式可以看出,权重系数 t 以指数等比形式缩小,时间越靠近当前时刻的数据加权影响力越大。 这个 t 越小,过去过去累计值的权重越低,当前抽样值的权重越高,平均值的实时性就越强。反之 t 越大,吸收瞬时突发值的能力变强,平均值的平稳性更好。

对于 jdk.ObjectAllocationSample 这个事件,算法实现即 jfrEventThrottler.hpp。如果大家感兴趣,可以在运行实例程序的时候,增加如下的启动参数 -Xlog:jfr+system+throttle=debug来查看这个 EWMA 采集窗口的相关信息,从而理解学习源码。日志示例:

[0.743s][debug][jfr,system,throttle] jdk.ObjectAllocationSample: avg.sample size: 0.0000, window set point: 0, sample size: 0, population size: 0, ratio: 0.0000, window duration: 0 ms
[1.761s][debug][jfr,system,throttle] jdk.ObjectAllocationSample: avg.sample size: 0.0400, window set point: 1, sample size: 1, population size: 19, ratio: 0.0526, window duration: 1000 ms
[2.775s][debug][jfr,system,throttle] jdk.ObjectAllocationSample: avg.sample size: 0.0784, window set point: 1, sample size: 1, population size: 19, ratio: 0.0526, window duration: 1000 ms
[3.794s][debug][jfr,system,throttle] jdk.ObjectAllocationSample: avg.sample size: 0.1153, window set point: 1, sample size: 1, population size: 118, ratio: 0.0085, window duration: 1000 ms
[4.815s][debug][jfr,system,throttle] jdk.ObjectAllocationSample: avg.sample size: 0.1507, window set point: 1, sample size: 1, population size: 107, ratio: 0.0093, window duration: 1000 ms


总结


  1. jdk.ObjectAllocationSample 是 Java 16 引入,用来优化对象分配不容易高效监控的事件。
  2. jdk.ObjectAllocationSample 事件里面的 ObjectClass 是触发事件的 class,weight 是线程分配的对象总大小。所以实际观察的时候,采集会与实际情况有些偏差。这是高效采集无法避免的。
  3. JVM 通过 EMWA 做了算法优化,采集到的事件随着程序运行会越来越是你的性能瓶颈或者热点代码相关的事件。
相关文章
|
14天前
|
Java 开发者 Spring
java springboot监听事件和处理事件
通过上述步骤,开发者可以在Spring Boot项目中轻松实现事件的发布和监听。事件机制不仅解耦了业务逻辑,还提高了系统的可维护性和扩展性。掌握这一技术,可以显著提升开发效率和代码质量。
77 33
|
6天前
|
算法 Java 编译器
深入理解 Java JDK —— 让我们从基础到进阶
JDK(Java Development Kit)是 Java 开发的核心工具包,包含编译、运行和调试 Java 程序所需的所有工具和库。它主要由 JVM(Java 虚拟机)、JRE(Java 运行时环境)和 Java 核心类库组成。JVM 是跨平台运行的基础,负责字节码的加载、执行和内存管理;JRE 提供运行 Java 应用的环境;核心类库则提供了丰富的 API 支持。通过编写、编译和运行一个简单的 Java 程序,可以深入理解 JDK 的工作原理。此外,JDK 还提供了 JIT 编译、垃圾回收优化和并发工具包等高级功能,帮助开发者提高程序性能和稳定性。
61 10
|
16天前
|
Java 开发者 Spring
java springboot监听事件和处理事件
通过上述步骤,开发者可以在Spring Boot项目中轻松实现事件的发布和监听。事件机制不仅解耦了业务逻辑,还提高了系统的可维护性和扩展性。掌握这一技术,可以显著提升开发效率和代码质量。
51 13
|
20天前
|
Java Spring
Java Spring Boot监听事件和处理事件
通过上述步骤,我们可以在Java Spring Boot应用中实现事件的发布和监听。事件驱动模型可以帮助我们实现组件间的松耦合,提升系统的可维护性和可扩展性。无论是处理业务逻辑还是系统事件,Spring Boot的事件机制都提供了强大的支持和灵活性。希望本文能为您的开发工作提供实用的指导和帮助。
72 15
|
22天前
|
Java 开发者 Spring
Java Springboot监听事件和处理事件
通过这些内容的详细介绍和实例解析,希望能帮助您深入理解Spring Boot中的事件机制,并在实际开发中灵活应用,提高系统的可维护性和扩展性。
54 7
|
2月前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
2月前
|
Oracle 安全 Java
深入理解Java生态:JDK与JVM的区分与协作
Java作为一种广泛使用的编程语言,其生态中有两个核心组件:JDK(Java Development Kit)和JVM(Java Virtual Machine)。本文将深入探讨这两个组件的区别、联系以及它们在Java开发和运行中的作用。
134 1
|
2月前
|
IDE Java 编译器
开发 Java 程序一定要安装 JDK 吗
开发Java程序通常需要安装JDK(Java Development Kit),因为它包含了编译、运行和调试Java程序所需的各种工具和环境。不过,某些集成开发环境(IDE)可能内置了JDK,或可使用在线Java编辑器,无需单独安装。
117 1
|
3月前
|
Java Spring 数据库连接
[Java]代理模式
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
62 0
[Java]代理模式
|
3月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
53 1