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(); } }