Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationInNewTLAB
重新申请 TLAB 分配对象事件:jdk.ObjectAllocationInNewTLAB
引入版本:Java 11
相关 ISSUES:
- JFR: RecordingStream leaks memory:启用 jdk.ObjectAllocationInNewTLAB 发现在 RecordingStream 中有内存泄漏,影响 Java 14、15、16,在 jdk-16+36 (Java 16.0.1) 修复。
- Introduce JFR Event Throttling and new jdk.ObjectAllocationSample event (enabled by default):引入 jdk.ObjectAllocationSample 优化并替代 jdk.ObjectAllocationInNewTLAB 和 jdk.ObjectAllocationOutsideTLAB 事件。
各版本配置:
从 Java 11 引入之后没有改变过:
默认配置(default.jfc of Java 11,default.jfc of Java 12,default.jfc of Java 13,default.jfc of Java 14,default.jfc of Java 15,default.jfc of Java 16,default.jfc of Java 17):
| 配置 | 值 | 描述 |
|---|---|---|
| enabled | false | 默认不启用 |
| stackTrace | true | 采集事件的时候,也采集堆栈 |
采样配置(profile.jfc of Java 11,profile.jfc of Java 12,profile.jfc of Java 13,profile.jfc of Java 14,profile.jfc of Java 15,profile.jfc of Java 16,profile.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 |
| tlabSize | 当前线程的 TLAB 大小 | 512.0 KB |
| 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();
recording.enable("jdk.ObjectAllocationInNewTLAB");
// JFR 记录启动
recording.start();
//强制 fullGC 防止接下来程序发生 GC
//同时可以区分出初始化带来的其他线程的TLAB相关的日志
whiteBox.fullGC();
//分配对象,大小1KB
for (int i = 0; i < 512; ++i) {
tmp = new byte[OBJECT_SIZE - BYTE_ARRAY_OVERHEAD];
}
//强制 fullGC,回收所有 TLAB
whiteBox.fullGC();
//分配对象,大小100KB
for (int i = 0; i < 200; ++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 countOf1KBObjectAllocationInNewTLAB = 0;
int countOf100KBObjectAllocationInNewTLAB = 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.ObjectAllocationInNewTLAB".equalsIgnoreCase(event.getEventType().getName())) {
if (allocationSize == 102400) {
countOf100KBObjectAllocationInNewTLAB++;
} else if (allocationSize == 1024) {
countOf1KBObjectAllocationInNewTLAB++;
}
} else {
throw new Exception("unexpected size of TLAB event");
}
System.out.println(event);
}
}
}
System.out.println("countOf1KBObjectAllocationInNewTLAB: " + countOf1KBObjectAllocationInNewTLAB);
System.out.println("countOf100KBObjectAllocationInNewTLAB: " + countOf100KBObjectAllocationInNewTLAB);
//阻塞程序,保证所有日志输出完
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.ObjectAllocationInNewTLAB {
//事件开始时间
startTime = 07:37:53.309
//分配对象类
objectClass = byte[] (classLoader = bootstrap)
//分配对象大小
allocationSize = 1.0 kB
//当前线程的 TLAB 大小
tlabSize = 457.7 kB
//事件发生所在线程
eventThread = "main" (javaThreadId = 1)
//事件发生所在堆栈
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 92
]
}
jdk.ObjectAllocationInNewTLAB {
startTime = 07:37:53.310
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 1.0 kB
tlabSize = 310.3 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 92
]
}
jdk.ObjectAllocationInNewTLAB {
startTime = 07:37:53.405
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
tlabSize = 512.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 98
]
}
jdk.ObjectAllocationInNewTLAB {
startTime = 07:37:53.409
objectClass = byte[] (classLoader = bootstrap)
allocationSize = 100.0 kB
tlabSize = 512.0 kB
eventThread = "main" (javaThreadId = 1)
stackTrace = [
com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 98
]
}
countOf1KBObjectAllocationInNewTLAB: 2
countOf100KBObjectAllocationInNewTLAB: 2
底层原理以及相关 JVM 源码
在每次发生内存分配的时候,都会创建一个 Allocation 对象记录描述本次分配的一些状态,他的构造函数以及析构函数为(其中 JFR 事件要采集的我已经注释出来了):
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_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 重分配,就会生成并采集一个 jdk.ObjectAllocationInNewTLAB 事件。
为何一般不在先生持续开启这个事件
这个事件配置项比较少,只要开启,就会发生一个 TLAB 重分配,就生成并采集一个 jdk.ObjectAllocationInNewTLAB 事件。对于大型项目来说,分析这个事件,如果没有堆栈,会很难定位。并且,并不是所有的 TLAB 重分配都是性能瓶颈,但是也无法简单的动态采集定位。如果需要动态开启采集,需要我们写额外的代码实现。如果开启堆栈采集,那么只要发生比较大量的 jdk.ObjectAllocationInNewTLAB 事件,就会成为性能瓶颈,因为堆栈采集是很耗费性能的。目前大部分的 Java 线上应用,尤其是微服务应用,都使用了各种框架,堆栈非常深,可能达到几百,如果涉及响应式编程,这个堆栈就更深了。JFR 考虑到这一点,默认采集堆栈深度最多是 64,即使是这样,也还是比较耗性能的。并且,在 Java 11 之后,JDK 一直在优化获取堆栈的速度,例如堆栈方法字符串放入缓冲池,优化缓冲池过期策略与 GC 策略等等,但是目前性能损耗还是不能忽视。
如果你不想开发额外代码,还想线上持续监控的话,建议使用 Java 16 引入的 jdk.ObjectAllocationSample
总结
- jdk.ObjectAllocationInNewTLAB 监控 TLAB 重分配事件,如果开启,只要发生 TLAB 重分配,就会生成并采集一个 jdk.ObjectAllocationInNewTLAB 事件。
- 开启采集,并打开堆栈采集的话,会非常消耗性能。
- 如果你不想开发额外代码,还想线上持续监控的话,建议使用 Java 16 引入的 jdk.ObjectAllocationSample
微信搜索“我的编程喵”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer:
Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationInNewTLAB的更多相关文章
- Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationOutsideTLAB
重新申请 TLAB 分配对象事件:jdk.ObjectAllocationOutsideTLAB 引入版本:Java 11 相关 ISSUES: JFR: RecordingStream leaks ...
- Java JFR 民间指南 - 事件详解 - jdk.ObjectAllocationSample
对象分配采样:jdk.ObjectAllocationSample 引入版本:Java 16 相关 ISSUE:Introduce JFR Event Throttling and new jdk.O ...
- Java JFR 民间指南 - 事件详解 - jdk.ThreadAllocationStatistics
定时线程分配统计事件:jdk.ThreadAllocationStatistics 引入版本:Java 11 相关 ISSUES: Test jdk/jfr/event/runtime/TestThr ...
- Java虚拟机之垃圾回收详解一
Java虚拟机之垃圾回收详解一 Java技术和JVM(Java虚拟机) 一.Java技术概述: Java是一门编程语言,是一种计算平台,是SUN公司于1995年首次发布.它是Java程序的技术基础,这 ...
- Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO
Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO Java 非阻塞 IO 和异步 IO 转自https://www.javadoop.com/post/nio-and-aio 本系 ...
- Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型
Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 ...
- JavaScript事件详解-jQuery的事件实现(三)
正文 本文所涉及到的jQuery版本是3.1.1,可以在压缩包中找到event模块.该篇算是阅读笔记,jQuery代码太长.... Dean Edward的addEvent.js 相对于zepto的e ...
- java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET
java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET 亲,“社区之星”已经一周岁了! 社区福利快来领取免费参加MDCC大会机会哦 Tag功能介绍—我们 ...
- Java Spring cron表达式使用详解
Java Spring cron表达式使用详解 By:授客 QQ:1033553122 语法格式 Seconds Minutes Hours DayofMonth Month DayofWeek ...
随机推荐
- windows(wsl)下的trino编译和升级注意事项
最近在进行旧版本的prestosql和prestodb升级相关的操作,尝试自己编译了一下,这里记录一下过程和遇到问题的处理. 因为Trino不支持windows下的编译,如果使用windows最方便的 ...
- nginx判断状态脚本
A是nginx行数 为0则启动nginx 启动失败则杀死keepalived进程
- R语言学习4:函数,流程控制,数据框重塑
本系列是一个新的系列,在此系列中,我将和大家共同学习R语言.由于我对R语言的了解也甚少,所以本系列更多以一个学习者的视角来完成. 参考教材:<R语言实战>第二版(Robert I.Kaba ...
- Jquery hover鼠标经过时弹出div动态提示语
一.效果图 二.需求描述 1.鼠标经过table每一行时,弹出div动态提示语: 2.div弹出层的位置随鼠标位置的变化而变化: 3.鼠标离开table或获取的动态提示语为空时,div弹出层消失. 下 ...
- 【ZeyFraのJavaEE开发小知识01】@DateTimeFomat和@JsonFormat
@DateTimeFormat 所在包:org.springframework.format.annotation.DateTimeFormat springframework的注解,一般用来对Dat ...
- 如何读写拥有命名空间xmlns 属性的Xml文件(C#实现)
我们在进行C#项目Xml读写开发时经常遇到一些读写问题,今天我要介绍的是遇到多个命名空间xmlns属性时如何读写此类文件. 比如下面这个Xml文件: <?xml version="1. ...
- XXL-JOB v2.3.0 发布 | 易用性增强
转: XXL-JOB v2.3.0 发布 | 易用性增强 v2.3.0 Release Notes 1.[新增]调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略.立即补偿触发一次等: 2. ...
- LeetCode-133克隆图(图的遍历+深拷贝概念)
克隆图 LeetCode-133 使用一个map来存储已经遍历的结点,这个存起来的结点必须是新new的才符合题意 /* // Definition for a Node. class Node { p ...
- 关于djangorestframework
djangorestframework技术文档 restfrmework规范 开发模式 普通开发为前端和后端代码放在一起写 前后端分离为前后端交互统统为ajax进行交互 前后端分离 优点:分工明细,节 ...
- VSCode添加用户代码片段,自定义用户代码片段
在使用VScode开发中经常会有一些重复使用的代码块,复制粘贴也很麻烦,这时可以在VScode中添加用户代码片段,输入简写即可快捷输入. VScode中添加用户自定义代码片段很简单. 1.在VScod ...
