重新申請 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: