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

引入版本: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
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 事件要采集的我已经注释出来了):

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_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

总结

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

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

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

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

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

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

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

  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. Spring Cloud基础

    1.网站架构演变过程 传统架构(单点应用SSM或SSH)→分布式架构(项目拆分)→SOA架构(面向服务架构)→微服务架构 2.微服务概述 2.1SOA架构 面向服务的架构(SOA)是一个组件模型,它将 ...

  2. Redis 内存淘汰机制详解

    一般来说,缓存的容量是小于数据总量的,所以,当缓存数据越来越多,Redis 不可避免的会被写满,这时候就涉及到 Redis 的内存淘汰机制了.我们需要选定某种策略将"不重要"的数据 ...

  3. ViewPager 高度自适应

    public class ContentViewPager extends ViewPager { public ContentViewPager(Context context) { super(c ...

  4. 使用 xunit 编写测试代码

    使用 xunit 编写测试代码 Intro xunit 是 .NET 里使用非常广泛的一个测试框架,有很多测试项目都是在使用 xunit 作为测试框架,不仅仅有很多开源项目在使用,很多微软的项目也在使 ...

  5. 后端程序员之路 46、Redis Sentinel

    Sentinel - Redis 命令参考http://doc.redisfans.com/topic/sentinel.html#sentinel-api Guidelines for Redis ...

  6. IDEA SVN 使用

    转: IDEA SVN 使用 一.上传项目到 SVN VCS -> Import into Version Control -> Share Project(Subversion) 点击 ...

  7. CentOS7 下 MySQL 5.7.23 & XtraBackup 24 做数据备份(1)——安装软件

    在两台机子上同时操作下面的步骤 首先安装MySQL,从官网下载相对应版本的RPM包 mysql-community-client-5.7.23-1.el7.x86_64.rpm mysql-commu ...

  8. Debian中的NVIDIA显卡驱动安装——超简单,一行命令

    其实Debian的non-free固件中包含NVIDIA的显卡驱动,所以没必要在官网下run包一步一步来 sudo apt install nvidia-settings 安装时会提示与X冲突,没关系 ...

  9. linux安装uwsgi,报错问题解决

    uwsgi安装 uwsgi启动后出 -- unavailable modifier requested: 0 出现问题的的原因是找不到python的解释器(其他语言同理) 你使用的yum instal ...

  10. Mongo的相关语法

    mongod的条件操作符 $gt -------- greater than > $gte --------- gt equal >= $lt -------- less than < ...