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] http://adoptopenjdk.net/jmc.html


後記

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

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