小師妹學JVM之:JIT中的LogCompilation

簡介

我們知道在JVM中爲了加快編譯速度,引入了JIT即時編譯的功能。那麼JIT什麼時候開始編譯的,又是怎麼編譯的,作爲一個高傲的程序員,有沒有辦法去探究JIT編譯的祕密呢?答案是有的,今天和小師妹一起帶大家來看一看這個編譯背後的祕密。

更多精彩內容且看:

LogCompilation簡介

小師妹:F師兄,JIT這麼神器,但是好像就是一個黑盒子,有沒有辦法可以探尋到其內部的本質呢?

追求真理和探索精神是我們作爲程序員的最大優點,想想如果沒有玻爾關於原子結構的新理論,怎麼會有原子體系的突破,如果沒有海森堡的矩陣力學,怎麼會有量子力學的建立?

JIT的編譯日誌輸出很簡單,使用 -XX:+LogCompilation就夠了。

如果要把日誌重定向到一個日誌文件中,則可以使用-XX:LogFile= 。

但是要開啓這些分析的功能,又需要使用-XX:+UnlockDiagnosticVMOptions。 所以總結一下,我們需要這樣使用:

-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=www.flydean.com.log

LogCompilation的使用

根據上面的介紹,我們現場來生成一個JIT的編譯日誌,爲了體現出專業性,這裏我們需要使用到JMH來做性能測試。

JMH的全稱是Java Microbenchmark Harness,是一個open JDK中用來做性能測試的套件。該套件已經被包含在了JDK 12中。

如果你使用的不是JDK 12,那麼需要添加如下依賴:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.19</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.19</version>
</dependency>

更多詳情可以參考我之前寫的: 在java中使用JMH(Java Microbenchmark Harness)做性能測試一文。

之前有的朋友說,代碼也用圖片,看起來好看,從本文之後,我們會盡量把代碼也轉成圖片來展示:

看完我的JMH的介紹,上面的例子應該很清楚了,主要就是做一個累加操作,然後warmup 5輪,測試5輪。

在@Fork註解裏面,我們可以配置jvm的參數,爲什麼我註釋掉了呢?因爲我發現在jvmArgsPrepend中的-XX:LogFile是不生效的。

沒辦法,我只好在運行配置中添加:

運行之後,你就可以得到輸出的編譯日誌文件。

解析LogCompilation文件

小師妹:F師兄,我看了一下生成的文件好複雜啊,用肉眼能看得明白嗎?

別怕,只是內容的多一點,如果我們細細再細細的分析一下,你會發現其實它真的非常非常…複雜!

其實寫點簡單的小白文不好嗎?爲什麼要來分析這麼複雜,又沒人看,看了也沒人懂的JVM底層…

大概,這就是專業吧!

LogCompilation文件其實是xml格式的,我們現在來大概分析一下,它的結構,讓大家下次看到這個文件也能夠大概瞭解它的重點。

首先最基本的信息就是JVM的信息,包括JVM的版本,JVM運行的參數,還有一些properties屬性。

我們收集到的日誌其實是分兩類的,第一類是應用程序本身的的編譯日誌,第二類就是編譯線程自己內部產生的日誌。

第二類的日誌會以hs_c*.log的格式存儲,然後在JVM退出的時候,再將這些文件跟最終的日誌輸出文件合併,生成一個整體的日誌文件。

比如下面的兩個就是編譯線程內部的日誌:

<thread_logfile thread='22275' filename='/var/folders/n5/217y_bgn49z18zvjch907xb00000gp/T//hs_c22275_pid83940.log'/>
<thread_logfile thread='41731' filename='/var/folders/n5/217y_bgn49z18zvjch907xb00000gp/T//hs_c41731_pid83940.log'/>

上面列出了編譯線程的id=22275,如果我們順着22275找下去,則可以找到具體編譯線程的日誌:

<compilation_log thread='22275'>
...
</compilation_log>

上面由compilation_log圍起來的部分就是編譯日誌了。

接下來的部分表示,編譯線程開始執行了,其中stamp表示的是啓動時間,下圖列出了一個完整的編譯線程的日誌:

<start_compile_thread name='C2 CompilerThread0' thread='22275' process='83940' stamp='0.058'/>

接下來描述的是要編譯的方法信息:

<task compile_id='10' method='java.lang.Object <init> ()V' bytes='1' count='1409' iicount='1409' stamp='0.153'>

上面列出了要編譯的方法名,compile_id表示的是系統內部分配的編譯id,bytes是方法中的字節數,count表示的是該方法的調用次數,注意,這裏的次數並不是方法的真實調用次數,只能做一個估計。

iicount是解釋器被調用的次數。

task執行了,自然就會執行完成,執行完成的內容是以task_done標籤來表示的:

<task_done success='1' nmsize='120' count='1468' stamp='0.155'/>

其中success表示是否成功執行,nmsize表示編譯器編譯出來的指令大小,以byte爲單位。如果有內聯的話,還有個inlined_bytes屬性,表示inlined的字節個數。

<type id='1025' name='void'/>

type表示的是方法的返回類型。

<klass id='1030' name='java.lang.Object' flags='1'/>

klass表示的是實例和數組類型。

<method id='1148' holder='1030' name='<init>' return='1025' flags='1' bytes='1' compile_id='1' compiler='c1' level='3' iicount='1419'/>

method表示執行的方法,holder是前面的klass的id,表示的是定義該方法的實例或者數組對象。method有名字,有
return,return對應的是上面的type。

flags表示的是方法的訪問權限。

接下來是parse,是分析階段的日誌:

<parse method='1148' uses='1419.000000' stamp='0.153'>

上面有parse的方法id。uses是使用次數。

<bc code='177' bci='0'/>

bc是byte Count的縮寫,code是byte的個數,bci是byte code的索引。

<dependency type='no_finalizable_subclasses' ctxk='1030'/>

dependency分析的是類的依賴關係,type表示的是什麼類型的依賴,ctkx是依賴的context class。

我們注意有的parse中,可能會有uncommon_trap:

<uncommon_trap bci='10' reason='unstable_if' action='reinterpret' debug_id='0' comment='taken never'/>

怎麼理解uncommon_trap呢?字面上意思就是捕獲非常用的代碼,就是說在解析代碼的過程中發現發現這些代碼是uncommon的,然後解析產生一個uncommon_trap,不再繼續進行了。

它裏面有兩個比較重要的字段,reason表示的是被標記爲uncommon_trap的原因。action表示的出發uncommon_trap的事件。

有些地方還會有call:

<call method='1150' count='5154' prof_factor='1.000000' inline='1'/>

call的意思是,在該代碼中將會調用其他的方法。count是執行次數。

總結

複雜的編譯日誌終於講完了,可能講的並不是很全,還有一些其他情況這裏並沒有列出來,後面如果遇到了,我再添加進去。

本文的例子https://github.com/ddean2009/learn-java-base-9-to-20

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-jit-logcompilation/

本文來源:flydean的博客

歡迎關注我的公衆號:程序那些事,更多精彩等着您!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章