每個程序員都該瞭解的JVM - 執行引擎
March 10, 2020我們在哪
介紹
我們知道類加載器加載完一個類別後 Java字節碼會存在方法區 然後由執行引擎一個指令一個指令執行類別中的方法
當然 Java的字節碼還是一種人類看得懂的語言 但機器無法直接執行 執行引擎必須把它轉成可以被JVM執行的語言 通過直譯器以及即時編譯器
直譯器
每跑一行就編譯一行 我們在JVM介紹中討論過 對於每條Java字節碼逐一轉譯 執行 性能較差
一般情況下 執行引擎都是先用直譯器來執行Java字節碼 等到某些條件成立後 才會使用JIT優化
JIT(Just-in-time compiler)即時編譯器
在虛擬機介紹前世今生中提到 虛擬機同時結合了編譯器的快速和直譯器的跨平台優點 但其實從JVM拿到bytecode之後 一開始是用直譯器一行一行跑 雖然直譯器跑得已經是相較底層的bytecode 但跟機器語言比起來還是慢很多 於是在比較常見的JVM實作中都會用即時編譯器來加速
加速方法很直觀 JIT會在所有需要執行的bytecode裡面找執行頻率高的 (比如說迴圈裡面的程式) 把這些bytecode編譯成機器語言之後存在高速緩存(Cache)裡 這樣只要每次一執行這部分的程式都是跑機器語言
上面那一段 可能是你隨口問你身旁的資深工程師會得到的答案 也是網路上最常見的答案
看完這個回答後我問了自己兩個問題
Q1.既然編譯完再跑快很多 那為什麼不把所有bytecode都編譯就好了?
A1.因為編譯需要花費額外的時間和記憶體 舉個例子 比如說某個函式 直譯器跑需要10ms 編譯需要100ms 跑機器語言需要2ms
如果這個函式只跑一次的話 用直譯器需要花10ms 用編譯需要花102ms 但如果這個函式跑100次 用直譯器需要1000ms 用編譯需要花300ms(加上一點點記憶體)
所以要是一段程式碼跑的頻率不高 那就不需要花費編譯的時間
Q2.我要怎麼知道一個方法會被跑幾次呢 編譯器才有可能先知道會被跑幾次 然後進而優化 一個直譯器就是看到什麼跑什麼 要怎麼辦呢?
A2.其實在開始執行bytecode的時候 都是用直譯器先跑 然後對於每一個方法都有一個計數器 每跑一次方法A就把方法A次數加一 當一個方法調用次數超過某個門檻 就調用編譯器 把編譯完的機器語言緩存下來 然後之後調用同一個方法的時候 用機器語言執行 然後調用次數再加一 直到超過第二個門檻的時候 再調用編輯器加上一些程度的最佳化 生成的機器語言複寫掉之前的 依此類推 直到某個門檻之後不能再最佳化了 就不再計數
這個方法又稱為動態編譯 因為這個方法大幅地加快了直譯器的速度 所以新版的javascript, python跟Ruby都有用JIT來加速直譯器
伏筆
你有沒有想過為什麼Oracle HotSpot叫做HotSpot呢? 這是因為Oracle出產的執行引擎可以找到具有最高編譯優先級的”熱點”代碼 先將這些熱點代碼編譯成machine code以後就輕鬆愉快
總結
本章說明了JVM是如何使用JIT來加速運行 讓Java達到高效能的目標