­

Java JFR 民間指南 – 事件詳解 – jdk.ObjectAllocationOutsideTLAB

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

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

image