Java Flight Recorder - 事件機制詳解

語言: CN / TW / HK
編者按: Java Flight Recorder(簡稱為JFR)曾經是 Oracle JDK 商業版的附屬元件,在 JDK 11 中正式開源,後又被移植到 JDK8 中。JFR對應用的侵入性很小,同時又能提供應用執行時相對準確和豐富的資訊;合理使用該工具可以極大地提高工作效率。本文剖析JFR的事件機制,希望能幫助大家從原理上理解 JFR ,進而能正確使用 JFR。


  1. 本篇文章中的原始碼大部分來自 openjdk8u262

  2. 本文出發點是梳理 JFR 的事件機制,側重點在於理解而非應用



對於JFR我們有著怎樣的預期

JFR是一個輔助分析工具,我們希望藉助它,儘可能低開銷地收集執行時資料,從而輔助對 系統(包括應用和JVM)可能存在的故障、效能瓶頸進行分析。
結合 JFR 的 目標來看:
  • 提供一些API用於產生資料或消費資料
  • 提供快取機制和二進位制資料格式

  • 允許配置和過濾事件

  • 為 OS、JVM、JDK 庫提供相應的事件
從中,我們能粗略地獲取這些資訊 :
  1. 事件以自描述的二進位制形式(.jfr)被儲存著
  2. 事件中包含了資料,事件 ≈ 資料

  3. .jfr 檔案 => read by some Provided API => 重現執行時資料 [ => 視覺化]

我們想嘗試瞭解 JFR的事件驅動機制,具體點就是回答幾個問題:
一個事件何時產生/啟動監控?經歷了怎樣的路徑?如何被儲存?儲存到哪裡?


JFR是事件驅動的

本節主要是一些前置資訊 (假如你有所瞭解,可以快速瀏覽或者跳過本節內容):JVM行為基本都是Event,如類載入對應著Class Load Event,垃圾回收對應GC Event;Event 主要由timestamp, event name, additional info, data 這幾部分組成。Event 收集四類事件的資訊:
  • Instant Event , 發生就收集(e.g. Thread Start ...)

  • Duration Event, 持續收集一段時間(e.g. GC Event ...)

  • Timed Event , 收集超過指定時間的事件

  • Sample Event , 按頻率取樣

以JFR的 Class Load Event 為例, 看看一個事件的結構。(共計24 bytes)

: 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00

  • Event Size : 98 80 80 00

  • Event ID : 87 02

  • TimeStamp : 95 ae e4 b2 92 03

  • Duration : a2 f7 ae 9a 94 02

  • Thread ID : 02

  • Stack trace ID : 01

  • PayLoad(記錄的資料,fields 取決於各個 Event 型別):

    • 載入的類 : 8d 11

    • 定義類的 ClassLoader : 00

    • 初始化類的 ClassLoader : 00

多個執行緒都會產生 Event,執行緒通過無鎖(Lock-free)設計記錄事件。執行緒將事件首先寫入到 ThreadLocalBuffer(簡稱TLB),TLB被填滿後,將被轉存到 Global buffer(circular),對於較舊的資料,可以通過配置,選擇丟棄或者寫入磁碟,以便連續儲存歷史記錄。示意圖如下所示:
注意:TLB、Global Buffer 和磁碟檔案中的事件記錄不會相互備份,未及時轉存的資料可能發生丟失,本文不會就這點展開闡述。JFR更多資訊可以參考JEP 328。
前置內容已經交代清楚,接著回到正軌。


一個事件的生命週期

以下是枯燥乏味的一堆程式碼,但是不得不看。首先來看 JFR 的結構,如下圖所示:
肉眼可見的一堆鉤子,這些hook 用於記錄對應的觸發事件。
我們簡單地挑一個 Thread Start 的事件,關注一下它的整個被觸發到被記錄的過程。線上程建立並執行時會呼叫記錄 JFR 事件,程式碼如下:
可見當一個新的Java 執行緒被建立時,只要開啟了 JFR,那麼就會執行上述程式碼;
接著看一下 on_thread_start 幹了什麼:
在此,我們看到了一個事件EventThreadStart ,並且在事件中設定資訊後被提交。
在  JEP 328 中有一個更為簡單直接例子,如下:
無需太過關心其內容。 我們只需關注這個事件生成的結構:
這裡的 EventType 定義於 jfrEventClass.hpp, 該檔案是編譯時生成的,簡單貼一下生成邏輯,可以參考 Makefile檔案,如下 (同樣無需在意太多細節):
回到主旋律,繼續來看事件的結構和成員函式,如下:
其中最為重要的成員函式是 JfrEvent::commit 方法,用於提交事件,程式碼如下:
在函式中,最後一段程式碼, 也是核心所在,用於真正記錄事件:
這下,就可以很容易地和第1節的內容對應上了,特別是其中的事件模型的圖片:



小結

使用者是否可以自定義一個JFR 事件?注意點有哪些?

這裡通過JEP 328 裡的例子(稍微有點改動),來展示如何自定義JFR 事件。
通過編譯後直接執行如下命令:
$> java -XX:StartFlightRecording,filename=event.jfr Test

可以得到如下日誌資訊:
   
   
   
Started recording 1. No limit specified, using maxsize=250MB as default. Use jcmd 57980 JFR.dump name=1 to copy recording data to file.

   
   
   
日誌可以通過標準的API 進行解析,下面通過一個簡單程式碼解析上面生成的事件,程式碼如下:
編譯執行
   
   
   
$> java Viewer | less
可以得到如下結果。
相信此時你已經對 JFR 的事件機制有了個不錯的感覺。
實際上JFR 的使用一般配合 JMC[1] 使用,在 JMC 中通過頁面可以得到統計資訊,更有助於判斷系統的執行情況。


參考

[1] https://adoptopenjdk.net/jmc.html


後記

如果遇到相關技術問題(包括不限於畢昇JDK),可以進入畢昇JDK社群查詢相關資源(點選原文進入官網),包括二進位制下載、程式碼倉庫、使用教學、安裝、學習資料等。畢昇JDK社群每雙週週二舉行技術例會,同時有一個技術交流群討論GCC、LLVM、JDK和V8等相關編譯技術,感興趣的同學可以新增如下微信小助手,回覆Compiler入群。

本文分享自微信公眾號 - openEuler(openEulercommunity)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。