重新申请 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. su: Authentication failure解决方法

    su命令不能切换root,提示su: Authentication failure,需要sudo passwd root一次之后,下次再su的时候只要输入密码就可以成功登录.

  2. Go的函数

    目录 Go的函数 一.函数的定义 1.函数的基本格式 2.函数的参数 2.1 函数传参的特点:copy传值 3.函数的返回值 4.可变长参数 二.函数的类型 1.给函数的类型重命名 三.匿名函数 1. ...

  3. 【转+】以C++为核心语言的高频交易系统的讨论

    [前言]高频交易是量化交易的核心.主要分两个方向:计算机技术和交易策略.策略各有不同,一般都是数据分析的专家或者金融,机器学习从业者.在计算机技术方面,一个是交易平台的性能,二者是硬件的性能,延时的多 ...

  4. Hexo一键部署到阿里云OSS并设置浏览器缓存

    自建博客地址:https://bytelife.net,欢迎访问! 本文为博客自动同步文章,为了更好的阅读体验,建议您移步至我的博客 本文作者: Jeffrey 本文链接: https://bytel ...

  5. 【转载】Android异步消息处理机制详解及源码分析

    PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbob ...

  6. HDR(高动态范围)

    一: 简介 一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的. 但是如果我们遇上了一个特定的区域,其中有多个亮光源使这些数值总和超过了1.0,又 ...

  7. 追溯 MySQL Statement Cancellation Timer

    原文 1. 背景 在 jstack 的内容中可以看到以下的 MySQL Statement Cancellation Timer 守护线程, 在业务高峰期的时候会出现大量的这类守护线程, 由此追溯该线 ...

  8. MySQL 多表查询与事务的操作

    表连接查询 什么是多表查询 # 数据准备 # 多表查询的作用 * 比如:我们想查询孙悟空的名字和他所在的部门的名字,则需要使用多表查询 # 如果一条 SQL 语句查询多张表,因为查询结果在多张不同的表 ...

  9. Python中类的特殊属性和魔术方法

    1.属性 属性 含义 __name__ 类.函数.方法等的名字   __dir__ __module__ 类定义所在的模块名 __class__ 对象或类所属的类   只是返回基类 __bases__ ...

  10. Java关于整型类缓存[-128,127]之间的数字

    我们在学习Java的包装类Integer.Long的时候可能会遇到这个问题: ①Integer a = 500;// Integer a = Integer.valueOf(500); 等价于上面的 ...