重新申请 TLAB 分配对象事件:jdk.ObjectAllocationOutsideTLAB

引入版本:Java 11

相关 ISSUES

  1. JFR: RecordingStream leaks memory:启用 jdk.ObjectAllocationInNewTLAB 发现在 RecordingStream 中有内存泄漏,影响 Java 14、15、16,在 jdk-16+36 (Java 16.0.1) 修复。
  2. Introduce JFR Event Throttling and new jdk.ObjectAllocationSample event (enabled by default):引入 jdk.ObjectAllocationSample 优化并替代 jdk.ObjectAllocationInNewTLAB 和 jdk.ObjectAllocationOutsideTLAB 事件。

各版本配置:

从 Java 11 引入之后没有改变过:

默认配置default.jfc of Java 11default.jfc of Java 12default.jfc of Java 13default.jfc of Java 14default.jfc of Java 15default.jfc of Java 16default.jfc of Java 17):

配置 描述
enabled false 默认不启用
stackTrace true 采集事件的时候,也采集堆栈

采样配置profile.jfc of Java 11profile.jfc of Java 12profile.jfc of Java 13profile.jfc of Java 14profile.jfc of Java 15profile.jfc of Java 16profile.jfc of Java 17):

配置 描述
enabled true 默认启用
stackTrace true 采集事件的时候,也采集堆栈

为何需要这个事件?

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

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

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

对于大部分的 JVM 应用,大部分的对象是在 TLAB 中分配的。如果 TLAB 外分配过多,或者 TLAB 重分配过多,那么我们需要检查代码,检查是否有大对象,或者不规则伸缩的对象分配,以便于优化代码。

事件包含属性

属性 说明 举例
startTime 事件开始时间 10:16:27.718
objectClass 触发本次事件的对象的类 byte[] (classLoader = bootstrap)
allocationSize 分配对象大小 10.0 MB
eventThread 事件发生所在线程 "Thread-0" (javaThreadId = 27)
stackTrace 事件发生所在堆栈

使用代码测试这个事件

package com.github.hashjang.jfr.test;

import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordedFrame;
import jdk.jfr.consumer.RecordingFile;
import sun.hotspot.WhiteBox; import java.io.File;
import java.nio.file.Path; public class TestAllocOutsideTLAB { //对于字节数组对象头占用16字节
private static final int BYTE_ARRAY_OVERHEAD = 16;
//我们要测试的对象大小是100kb
private static final int OBJECT_SIZE = 1024;
//字节数组对象名称
private static final String BYTE_ARRAY_CLASS_NAME = new byte[0].getClass().getName(); //需要使用静态field,而不是方法内本地变量,否则编译后循环内的new byte[]全部会被省略,只剩最后一次的
public static byte[] tmp; public static void main(String[] args) throws Exception {
WhiteBox whiteBox = WhiteBox.getWhiteBox();
//初始化 JFR 记录
Recording recording = new Recording();
//启用 jdk.ObjectAllocationOutsideTLAB 事件监控
recording.enable("jdk.ObjectAllocationOutsideTLAB");
// JFR 记录启动
recording.start();
//强制 fullGC 防止接下来程序发生 GC
//同时可以区分出初始化带来的其他线程的TLAB相关的日志
whiteBox.fullGC();
//分配对象,大小1KB
for (int i = 0; i < 2048; ++i) {
tmp = new byte[OBJECT_SIZE - BYTE_ARRAY_OVERHEAD];
}
//强制 fullGC,回收所有 TLAB
whiteBox.fullGC();
//分配对象,大小100KB
for (int i = 0; i < 10; ++i) {
tmp = new byte[OBJECT_SIZE * 100 - BYTE_ARRAY_OVERHEAD];
}
whiteBox.fullGC();
//将 JFR 记录 dump 到一个文件
Path path = new File(new File(".").getAbsolutePath(), "recording-" + recording.getId() + "-pid" + ProcessHandle.current().pid() + ".jfr").toPath();
recording.dump(path);
int countOf1KBObjectAllocationOutsideTLAB = 0;
int countOf100KBObjectAllocationOutsideTLAB = 0;
//读取文件中的所有 JFR 事件
for (RecordedEvent event : RecordingFile.readAllEvents(path)) {
//获取分配的对象的类型
String className = event.getString("objectClass.name"); if (
//确保分配类型是 byte[]
BYTE_ARRAY_CLASS_NAME.equalsIgnoreCase(className)
) {
RecordedFrame recordedFrame = event.getStackTrace().getFrames().get(0);
//同时必须是咱们这里的main方法分配的对象,并且是Java堆栈中的main方法
if (recordedFrame.isJavaFrame()
&& "main".equalsIgnoreCase(recordedFrame.getMethod().getName())
) {
//获取分配对象大小
long allocationSize = event.getLong("allocationSize");
//统计各种事件个数
if ("jdk.ObjectAllocationOutsideTLAB".equalsIgnoreCase(event.getEventType().getName())) {
if (allocationSize == 102400) {
countOf100KBObjectAllocationOutsideTLAB++;
} else if (allocationSize == 1024) {
countOf1KBObjectAllocationOutsideTLAB++;
}
} else {
throw new Exception("unexpected size of TLAB event");
}
System.out.println(event);
}
}
}
System.out.println("countOf1KBObjectAllocationOutsideTLAB: " + countOf1KBObjectAllocationOutsideTLAB);
System.out.println("countOf100KBObjectAllocationOutsideTLAB: " + countOf100KBObjectAllocationOutsideTLAB);
//阻塞程序,保证所有日志输出完
Thread.currentThread().join();
}
}

以下面参数运行这个程序,注意将 whitebox jar 包位置参数替换成你的 whitebox jar 包所在位置。

-Xbootclasspath/a:D:\github\jfr-spring-all\jdk-white-box\target\jdk-white-box-17.0-SNAPSHOT.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xms512m -Xmx512m

运行结果:

jdk.ObjectAllocationOutsideTLAB {
//事件开始时间
startTime = 08:56:49.220
//分配对象类
objectClass = byte[] (classLoader = bootstrap)
//分配对象大小
allocationSize = 100.0 kB
//事件发生所在线程
eventThread = "main" (javaThreadId = 1)
//事件发生所在堆栈
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 95
]
} jdk.ObjectAllocationOutsideTLAB {
startTime = 08:56:49.220
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 95
]
} jdk.ObjectAllocationOutsideTLAB {
startTime = 08:56:49.220
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 95
]
} jdk.ObjectAllocationOutsideTLAB {
startTime = 08:56:49.220
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 95
]
} jdk.ObjectAllocationOutsideTLAB {
startTime = 08:56:49.220
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 95
]
} countOf1KBObjectAllocationOutsideTLAB: 0
countOf100KBObjectAllocationOutsideTLAB: 5

底层原理以及相关 JVM 源码

在每次发生内存分配的时候,都会创建一个 Allocation 对象记录描述本次分配的一些状态,他的构造函数以及析构函数为(其中 JFR 事件要采集的我已经注释出来了):

memAllocator.cpp

public:
Allocation(const MemAllocator& allocator, oop* obj_ptr)
//内存分配器
: _allocator(allocator),
//分配线程
_thread(Thread::current()),
//要分配的对象指针
_obj_ptr(obj_ptr),
_overhead_limit_exceeded(false),
//是否是 tlab 外分配
_allocated_outside_tlab(false),
//本次分配新分配的 tlab 大小,只有发生 tlab 重分配这个值才会大于 0
_allocated_tlab_size(0),
_tlab_end_reset_for_sample(false)
{
verify_before();
} ~Allocation() {
if (!check_out_of_memory()) {
verify_after();
//在销毁时,调用 notify_allocation 来上报相关采集
notify_allocation();
}
}

notify_allocation()包括:

void MemAllocator::Allocation::notify_allocation() {
notify_allocation_low_memory_detector();
//上报 jfr 相关
notify_allocation_jfr_sampler();
notify_allocation_dtrace_sampler();
notify_allocation_jvmti_sampler();
} void MemAllocator::Allocation::notify_allocation_jfr_sampler() {
HeapWord* mem = cast_from_oop<HeapWord*>(obj());
size_t size_in_bytes = _allocator._word_size * HeapWordSize;
//如果标记的是 tlab 外分配,调用 send_allocation_outside_tlab
if (_allocated_outside_tlab) {
AllocTracer::send_allocation_outside_tlab(obj()->klass(), mem, size_in_bytes, _thread);
} else if (_allocated_tlab_size != 0) {
//如果不是 tlab 外分配,并且 _allocated_tlab_size 大于 0,代表发生了 tlab 重分配,调用 send_allocation_outside_tlab
AllocTracer::send_allocation_in_new_tlab(obj()->klass(), mem, _allocated_tlab_size * HeapWordSize,
size_in_bytes, _thread);
}
}

在发生 TLAB 外分配的时候,会立刻生成这个事件并上报,对应源码:

allocTracer.cpp

//在每次发生 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);
}

通过源码分析我们可以知道,如果开启这个事件,那么只要发生 TLAB 外分配,就会生成并采集一个 jdk.ObjectAllocationOutsideTLAB 事件

为何一般不在先生持续开启这个事件

这个事件配置项比较少,只要开启,就会发生一个 TLAB 外分配,就生成并采集一个 jdk.ObjectAllocationOutsideTLAB 事件。对于大型项目来说,分析这个事件,如果没有堆栈,会很难定位。并且,TLAB 外分配如果发生的话,就会连续比较大量发生,采集这个事件会进一步增加性能消耗,但是也无法简单的动态采集定位。如果需要动态开启采集,需要我们写额外的代码实现。如果开启堆栈采集,那么只要发生比较大量的 jdk.ObjectAllocationInNewTLAB 事件,就会成为性能瓶颈,因为堆栈采集是很耗费性能的。目前大部分的 Java 线上应用,尤其是微服务应用,都使用了各种框架,堆栈非常深,可能达到几百,如果涉及响应式编程,这个堆栈就更深了。JFR 考虑到这一点,默认采集堆栈深度最多是 64,即使是这样,也还是比较耗性能的。并且,在 Java 11 之后,JDK 一直在优化获取堆栈的速度,例如堆栈方法字符串放入缓冲池,优化缓冲池过期策略与 GC 策略等等,但是目前性能损耗还是不能忽视。

如果你不想开发额外代码,还想线上持续监控的话,建议使用 Java 16 引入的 jdk.ObjectAllocationSample

总结

  1. jdk.jdk.ObjectAllocationOutsideTLAB 监控 TLAB 外分配事件,如果开启,只要发生 TLAB 外分配,就会生成并采集一个 jdk.ObjectAllocationOutsideTLAB 事件。
  2. 开启采集,并打开堆栈采集的话,会非常消耗性能。
  3. 如果你不想开发额外代码,还想线上持续监控的话,建议使用 Java 16 引入的 jdk.ObjectAllocationSample

微信搜索“我的编程喵”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer

Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationOutsideTLAB的更多相关文章

  1. Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationSample

    对象分配采样:jdk.ObjectAllocationSample 引入版本:Java 16 相关 ISSUE:Introduce JFR Event Throttling and new jdk.O ...

  2. Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationInNewTLAB

    重新申请 TLAB 分配对象事件:jdk.ObjectAllocationInNewTLAB 引入版本:Java 11 相关 ISSUES: JFR: RecordingStream leaks me ...

  3. Java JFR 民间指南 - 事件详解 - jdk.ThreadAllocationStatistics

    定时线程分配统计事件:jdk.ThreadAllocationStatistics 引入版本:Java 11 相关 ISSUES: Test jdk/jfr/event/runtime/TestThr ...

  4. Java虚拟机之垃圾回收详解一

    Java虚拟机之垃圾回收详解一 Java技术和JVM(Java虚拟机) 一.Java技术概述: Java是一门编程语言,是一种计算平台,是SUN公司于1995年首次发布.它是Java程序的技术基础,这 ...

  5. Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO

    Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO Java 非阻塞 IO 和异步 IO 转自https://www.javadoop.com/post/nio-and-aio 本系 ...

  6. Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型

    Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 ...

  7. JavaScript事件详解-jQuery的事件实现(三)

    正文 本文所涉及到的jQuery版本是3.1.1,可以在压缩包中找到event模块.该篇算是阅读笔记,jQuery代码太长.... Dean Edward的addEvent.js 相对于zepto的e ...

  8. java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET

    java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET 亲,“社区之星”已经一周岁了!      社区福利快来领取免费参加MDCC大会机会哦    Tag功能介绍—我们 ...

  9. Java Spring cron表达式使用详解

    Java Spring cron表达式使用详解   By:授客 QQ:1033553122 语法格式 Seconds Minutes Hours DayofMonth Month DayofWeek ...

随机推荐

  1. Linux常用小命令

    1:查看当前磁盘内存 df-ah/df-hl 2:查看文件和文件夹大小 du -h --max-depth=1 /目的文件夹 3:scp 拷贝命令 指定端口传输文件 scp -p port filen ...

  2. SpringBoot Admin应用监控搭建

    简介 Spring Boot Admin 用于监控基于 Spring Boot 的应用,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI. 参考手册地址:htt ...

  3. linux之安装nginx

    nginx官网:http://nginx.org/en/download.html 1.安装nginx所需环境 a)  PCRE pcre-devel 安装 # yum install -y pcre ...

  4. python进阶(11)生成器

    生成器 利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成.但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据. ...

  5. 基于docker创建Cassandra集群

    一.概述 简介 Cassandra是一个开源分布式NoSQL数据库系统. 它最初由Facebook开发,用于储存收件箱等简单格式数据,集GoogleBigTable的数据模型与Amazon Dynam ...

  6. PVE更新WEB管理地址

    PVE也是一台Linux系统,如果PVE更换了网络环境,比如从家里拿到了办公室,那么就需要对其更新网络,才能让其它机器访问到它的8006管理地址. 具体做法是通过修改配置文件来更改IP. 更新网卡配置 ...

  7. JS动态获取select中被选中的option的值,并在控制台输出

    生活城市: <select id="province"> <option>河南省</option> <option>黑龙江省< ...

  8. 痞子衡嵌入式:盘点国内Cortex-M内核MCU厂商高性能产品

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是国内Cortex-M内核MCU厂商高性能产品. 在8/16位中低端MCU领域,国内厂商的本土化产品设计以及超低价特点,使得其与国外大厂竞 ...

  9. MySQL中如何查询中位数

    员工薪水中位数 题目描述: 预期答案: 解法1 既然是求解中位数,我们首先想到的是根据中位数的定义进行求解:奇数个数字时,中位数是中间的数字:偶数个数字时,中位数中间两个数的均值.本题不进行求解均值, ...

  10. JavaCV 采集摄像头及桌面视频数据

    javacv 封装了javacpp-presets库很多native API,简化了开发,对java程序员来说比较友好. 之前使用JavaCV库都是使用ffmpeg native API开发,这种方式 ...