Java JFR 民間指南 - 事件詳解 - jdk.ObjectAllocationInNewTLAB

乾貨滿滿張雜湊發表於2021-04-23

重新申請 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 物件分配的流程:

image

對於 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

image

相關文章