導航:首頁 > 源碼編譯 > jit即時編譯器作用

jit即時編譯器作用

發布時間:2023-04-23 20:54:50

1. java中的JIT編譯到底是什麼意思

在JIT編譯器生成本機代碼後,它會重寫存根常式,插入一個jmp指令跳轉到剛才JIT編譯器的代碼。只有當要調用某個方法時,JIT編譯器才會將CIL的方法體編譯為相應的本機機器碼版本。這樣可以優化程序的工作集。

2. 了解什麼叫做jit compiling,與傳統的編譯技術有何不同

Java 應用程序的性能經常成為開發社區中的討論熱點。因為該語言的設計初衷是使用解釋的方式支持應用程序的可移植性目標,早期
Java 運行時所提供的性能級別遠低於 C 和
C++
之類的編譯語言。盡管這些語言可以提供更高的性能,但是生成的代碼只能在有限的幾種系統上執行。在過去的十年中,Java
運行時供應商開發了一些復雜的動態編譯器,通常稱作即時(Just-in-time,JIT)編譯器。程序運行時,JIT
編譯器選擇將最頻繁執行的方法編譯成本地代碼。運行時才進行本地代碼編譯而不是在程序運行前進行編譯(用 C 或
C++ 編寫的程序正好屬於後一情形),保證了可移植性的需求。有些 JIT 編譯器甚至不使用解釋程序就能編譯所有的代碼,但是這些編譯器仍然通過在程序執行時進行一些操作來保持 Java 應用程序的可移植性。
由於動態編譯技術的多項改進,在很多應用程序中,現代的 JIT 編譯器可以產生與 C 或 C++
靜態編譯相當的應用程序性能。但是,仍然有很多軟體開發人員認為 —— 基於經驗或者傳聞 ——
動態編譯可能嚴重干擾程序操作,因為編譯器必須與應用程序共享 CPU。一些開發人員強烈呼籲對 Java
代碼進行靜態編譯,並且堅信那樣可以解決性能問題。對於某些應用程序和執行環境而言,這種觀點是正確的,靜態編譯可以極大地提高 Java
性能,或者說它是惟一的實用選擇。但是,靜態地編譯 Java 應用程序在獲得高性能的同時也帶來了很多復雜性。一般的
Java 開發人員可能並沒有充分地感受到 JIT 動態編譯器的優點。

本文考察了 Java 語言靜態編譯和動態編譯所涉及的一些問題,重點介紹了實時 (RT) 系統。簡要描述了 Java
語言解釋程序的操作原理並說明了現代 JIT 編譯器執行本地代碼編譯的優缺點。介紹了 IBM 在 WebSphere Real Time 中發布的
AOT 編譯技術和它的一些優缺點。然後比較了這兩種編譯策略並指出了幾種比較適合使用 AOT
編譯的應用程序領域和執行環境。要點在於這兩種編譯技術並不互斥:即使在使用這兩種技術最為有效的各種應用程序中,它們也分別存在一些影響應用程序的優缺
點。

執行 Java 程序

Java 程序最初是通過 Java SDK 的 javac程序編譯成本地的與平台無關的格式(類文件)。可將此格式看作 Java
平台,因為它定義了執行 Java 程序所需的所有信息。Java 程序執行引擎,也稱作 Java 運行時環境(JRE),包含了為特定的本地平台實現
Java 平台的虛擬機。例如,基於 Linux 的 Intel x86 平台、Sun Solaris 平台和 AIX 操作系統上運行的 IBM
System p 平台,每個平台都擁有一個 JRE。這些 JRE 實現實現了所有的本地支持,從而可以正確執行為
Java 平台編寫的程序。

事實上,操作數堆棧的大小有實際限制,但是編程人員極少編寫超出該限制的方法。JVM 提供了安全性檢查,對那些創建出此類方法的編程人員進行通知。

Java 平台程序表示的一個重要部分是位元組碼序列,它描述了 Java
類中每個方法所執行的操作。位元組碼使用一個理論上無限大的操作數堆棧來描述計算。這個基於堆棧的程序表示提供了平台無關性,因為它不依賴任何特定本地平台
的 CPU 中可用寄存器的數目。可在操作數堆棧上執行的操作的定義都獨立於所有本地處理器的指令集。Java
虛擬機(JVM)規范定義了這些位元組碼的執行(參見 參考資料)。執行 Java 程序時,用於任何特定本地平台的任何 JRE 都必須遵守 JVM
規范中列出的規則。

因為基於堆棧的本地平台很少(Intel X87 浮點數協處理器是一個明顯的例外),所以大多數本地平台不能直接執行 Java 位元組碼。為了解決這個問題,早期的 JRE 通過解釋位元組碼來執行 Java 程序。即 JVM 在一個循環中重復操作:

◆獲取待執行的下一個位元組碼;

◆解碼;

◆從操作數堆棧獲取所需的操作數;

◆按照 JVM 規范執行操作;

◆將結果寫回堆棧。

這種方法的優點是其簡單性:JRE 開發人員只需編寫代碼來處理每種位元組碼即可。並且因為用於描述操作的位元組碼少於 255 個,所以實現的成本比較低。當然,缺點是性能:這是一個早期造成很多人對 Java 平台不滿的問題,盡管擁有很多其他優點。

解決與 C 或 C++ 之類的語言之間的性能差距意味著,使用不會犧牲可移植性的方式開發用於 Java 平台的本地代碼編譯。

編譯 Java 代碼

盡管傳聞中 Java 編程的 「一次編寫,隨處運行」
的口號可能並非在所有情況下都嚴格成立,但是對於大量的應用程序來說情況確實如此。另一方面,本地編譯本質上是特定於平台的。那麼 Java
平台如何在不犧牲平台無關性的情況下實現本地編譯的性能?答案就是使用 JIT 編譯器進行動態編譯,這種方法已經使用了十年(參見圖 1):

圖 1. JIT 編譯器

使用 JIT 編譯器時,Java
程序按每次編譯一個方法的形式進行編譯,因為它們在本地處理器指令中執行以獲得更高的性能。此過程將生成方法的一個內部表示,該表示與位元組碼不同但是其級
別要高於目標處理器的本地指令。(IBM JIT
編譯器使用一個表達式樹序列表示方法的操作。)編譯器執行一系列優化以提高質量和效率,最後執行一個代碼生成步驟將優化後的內部表示轉換成目標處理器的本
地指令。生成的代碼依賴運行時環境來執行一些活動,比如確保類型轉換的合法性或者對不能在代碼中直接執行的某些類型的對象進行分配。JIT
編譯器操作的編譯線程與應用程序線程是分開的,因此應用程序不需要等待編譯的執行。

圖 1 中還描述了用於觀察執行程序行為的分析框架,通過周期性地對線程取樣找出頻繁執行的方法。該框架還為專門進行分析的方法提供了工具,用來存儲程序的此次執行中可能不會改變的動態值。

因為這個 JIT 編譯過程在程序執行時發生,所以能夠保持平台無關性:發布的仍然是中立的 Java 平台代碼。C 和 C++ 之類的語言缺乏這種優點,因為它們在程序執行前進行本地編譯;發布給(本地平台)執行環境的是本地代碼。

挑戰

盡管通過 JIT 編譯保持了平台無關性,但是付出了一定代價。因為在程序執行時進行編譯,所以編譯代碼的時間將計入程序的執行時間。任何編寫過大型 C 或 C++ 程序的人都知道,編譯過程往往較慢。

為了克服這個缺點,現代的 JIT
編譯器使用了下面兩種方法的任意一種(某些情況下同時使用了這兩種方法)。第一種方法是:編譯所有的代碼,但是不執行任何耗時多的分析和轉換,因此可以快
速生成代碼。由於生成代碼的速度很快,因此盡管可以明顯觀察到編譯帶來的開銷,但是這很容易就被反復執行本地代碼所帶來的性能改善所掩蓋。第二種方法是:
將編譯資源只分配給少量的頻繁執行的方法(通常稱作熱方法)。低編譯開銷更容易被反復執行熱代碼帶來的性能優勢掩蓋。很多應用程序只執行少量的熱方法,因
此這種方法有效地實現了編譯性能成本的最小化。

動態編譯器的一個主要的復雜性在於權衡了解編譯代碼的預期獲益使方法的執行對整個程序的性能起多大作用。一個極端的例子是,程序執行後,您非常清楚哪些方
法對於這個特定的執行的性能貢獻最大,但是編譯這些方法毫無用處,因為程序已經完成。而在另一個極端,程序執行前無法得知哪些方法重要,但是每種方法的潛
在受益都最大化了。大多數動態編譯器的操作介於這兩個極端之間,方法是權衡了解方法預期獲益的重要程度。

Java 語言需要動態載入類這一事實對 Java
編譯器的設計有著重要的影響。如果待編譯代碼引用的其他類還沒有載入怎麼辦?比如一個方法需要讀取某個尚未載入的類的靜態欄位值。Java
語言要求第一次執行類引用時載入這個類並將其解析到當前的 JVM
中。直到第一次執行時才解析引用,這意味著沒有地址可供從中載入該靜態欄位。編譯器如何處理這種可能性?編譯器生成一些代碼,用於在沒有載入類時載入並解
析類。類一旦被解析,就會以一種線程安全的方式修改原始代碼位置以便直接訪問靜態欄位的地址,因為此時已獲知該地址。

IBM JIT
編譯器中進行了大量的努力以便使用安全而有效率的代碼補丁技術,因此在解析類之後,執行的本地代碼只載入欄位的值,就像編譯時已經解析了欄位一樣。另外一
種方法是生成一些代碼,用於在查明欄位的位置以前一直檢查是否已經解析欄位,然後載入該值。對於那些由未解析變成已解析並被頻繁訪問的欄位來說,這種簡單
的過程可能帶來嚴重的性能問題。

動態編譯的優點

動態地編譯 Java 程序有一些重要的優點,甚至能夠比靜態編譯語言更好地生成代碼,現代的 JIT 編譯器常常向生成的代碼中插入掛鉤以收集有關程序行為的信息,以便如果要選擇方法進行重編譯,就可以更好地優化動態行為。

關於此方法的一個很好的例子是收集一個特定 array操作的長度。如果發現每次執行操作時該長度基本不變,則可以為最頻繁使用的

array長度生成專門的代碼,或者可以調用調整為該長度的代碼序列。由於內存系統和指令集設計的特性,用於復制內存的最佳通用常式的執行速度通
常比用於復制特定長度的代碼慢。例如,復制 8
個位元組的對齊的數據可能需要一到兩條指令直接復制,相比之下,使用可以處理任意位元組數和任意對齊方式的一般復制循環可能需要 10 條指令來復制同樣的 8

個位元組。但是,即使此類專門的代碼是為某個特定的長度生成的,生成的代碼也必須正確地執行其他長度的復制。生成代碼只是為了使常見長度的操作執行得更快,
因此平均下來,性能得到了改進。此類優化對大多數靜態編譯語言通常不實用,因為所有可能的執行中長度恆定的操作比一個特定程序執行中長度恆定的操作要少得
多。

此類優化的另一個重要的例子是基於類層次結構的優化。例如,一個虛方法調用需要查看接收方對象的類調用,以便找出哪個實際目標實現了接收方對象的虛方法。
研究表明:大多數虛調用只有一個目標對應於所有的接收方對象,而 JIT
編譯器可以為直接調用生成比虛調用更有效率的代碼。通過分析代碼編譯後類層次結構的狀態,JIT
編譯器可以為虛調用找到一個目標方法,並且生成直接調用目標方法的代碼而不是執行較慢的虛調用。當然,如果類層次結構發生變化,並且出現另外的目標方法,
則 JIT
編譯器可以更正最初生成的代碼以便執行虛調用。在實踐中,很少需要作出這些更正。另外,由於可能需要作出此類更正,因此靜態地執行這種優化非常麻煩。

因為動態編譯器通常只是集中編譯少量的熱方法,所以可以執行更主動的分析來生成更好的代碼,使編譯的回報更高。事實上,大部分現代的
JIT
編譯器也支持重編譯被認為是熱方法的方法。可以使用靜態編譯器(不太強調編譯時間)中常見的非常主動的優化來分析和轉換這些頻繁執行的方法,以便生成更好
的代碼並獲得更高的性能。

這些改進及其他一些類似的改進所產生的綜合效果是:對於大量的 Java 應用程序來說,動態編譯已經彌補了與 C 和 C++ 之類語言的靜態本地編譯性能之間的差距,在某些情況下,甚至超過了後者的性能。

缺點

但是,動態編譯確實具有一些缺點,這些缺點使它在某些情況下算不上一個理想的解決方案。例如,因為識別頻繁執行的方法以及編譯這些方法需要時間,所以應用
程序通常要經歷一個准備過程,在這個過程中性能無法達到其最高值。在這個准備過程中出現性能問題有幾個原因。首先,大量的初始編譯可能直接影響應用程序的
啟動時間。不僅這些編譯延遲了應用程序達到穩定狀態的時間(想像 Web
伺服器經
歷一個初始階段後才能夠執行實際有用的工作),而且在准備階段中頻繁執行的方法可能對應用程序的穩定狀態的性能所起的作用也不大。如果 JIT
編譯會延遲啟動又不能顯著改善應用程序的長期性能,則執行這種編譯就非常浪費。雖然所有的現代 JVM
都執行調優來減輕啟動延遲,但是並非在所有情況下都能夠完全解決這個問題。

其次,有些應用程序完全不能忍受動態編譯帶來的延遲。如 GUI 介面之類互動式應用程序就是這樣的例子。在這種情況下,編譯活動可能對用戶使用造成不利影響,同時又不能顯著地改善應用程序的性能。

最後,用於實時環境並具有嚴格的任務時限的應用程序可能無法忍受編譯的不確定性性能影響或動態編譯器本身的內存開銷。

因此,雖然 JIT 編譯技術已經能夠提供與靜態語言性能相當(甚至更好)的性能水平,但是動態編譯並不適合於某些應用程序。在這些情況下,Java 代碼的提前(Ahead-of-time,AOT)編譯可能是合適的解決方案。

AOT Java 編譯

大致說來,Java 語言本地編譯應該是為傳統語言(如 C++ 或
Fortran)而開發的編譯技術的一個簡單應用。不幸的是,Java 語言本身的動態特性帶來了額外的復雜性,影響了 Java
程序靜態編譯代碼的質量。但是基本思想仍然是相同的:在程序執行前生成 Java 方法的本地代碼,以便在程序運行時直接使用本地代碼。目的在於避免
JIT 編譯器的運行時性能消耗或內存消耗,或者避免解釋程序的早期性能開銷。

挑戰

動態類載入是動態 JIT 編譯器面臨的一個挑戰,也是 AOT
編譯的一個更重要的問題。只有在執行代碼引用類的時候才載入該類。因為是在程序執行前進行 AOT
編譯的,所以編譯器無法預測載入了哪些類。就是說編譯器無法獲知任何靜態欄位的地址、任何對象的任何實例欄位的偏移量或任何調用的實際目標,甚至對直接調
用(非虛調用)也是如此。在執行代碼時,如果證明對任何這類信息的預測是錯誤的,這意味著代碼是錯誤的並且還犧牲了 Java 的一致性。

因為代碼可以在任何環境中執行,所以類文件可能與代碼編譯時不同。例如,一個 JVM
實例可能從磁碟的某個特定位置載入類,而後面一個實例可能從不同的位置甚至網路載入該類。設想一個正在進行 bug
修復的開發環境:類文件的內容可能隨不同的應用程序的執行而變化。此外,Java 代碼可能在程序執行前根本不存在:比如 Java
反射服務通常在運行時生成新類來支持程序的行為。

缺少關於靜態、欄位、類和方法的信息意味著嚴重限制了 Java 編譯器中優化框架的大部分功能。內聯可能是靜態或動態編譯器應用的最重要的優化,但是由於編譯器無法獲知調用的目標方法,因此無法再使用這種優化。

內聯

內聯是一種用於在運行時生成代碼避免程序開始和結束時開銷的技術,方法是將函數的調用代碼插入到調用方的函數中。但是內聯最大的益處可能是優化方可見的代碼的范圍擴大了,從而能夠生成更高質量的代碼。下面是一個內聯前的代碼示例:

int foo() { int x=2, y=3; return bar(x,y); }final int bar(int a, int b) { return a+b; }

如果編譯器可以證明這個 bar就是 foo()中調用的那個方法,則 bar中的代碼可以取代 foo()中對
bar()的調用。這時,bar()方法是 final類型,因此肯定是 foo()中調用的那個方法。甚至在一些虛調用例子中,動態 JIT
編譯器通常能夠推測性地內聯目標方法的代碼,並且在絕大多數情況下能夠正確使用。編譯器將生成以下代碼:

int foo() { int x=2, y=3; return x+y; }

在這個例子中,簡化前名為值傳播的優化可以生成直接返回
5的代碼。如果不使用內聯,則不能執行這種優化,產生的性能就會低很多。如果沒有解析
bar()方法(例如靜態編譯),則不能執行這種優化,而代碼必須執行虛調用。運行時,實際調用的可能是另外一個執行兩個數字相乘而不是相加的
bar方法。所以不能在 Java 程序的靜態編譯期間直接使用內聯。

AOT
代碼因此必須在沒有解析每個靜態、欄位、類和方法引用的情況下生成。執行時,每個這些引用必須利用當前運行時環境的正確值進行更新。這個過程可能直接影響
第一次執行的性能,因為在第一次執行時將解析所有引用。當然,後續執行將從修補代碼中獲益,從而可以更直接地引用實例、靜態欄位或方法目標。

另外,為 Java 方法生成的本地代碼通常需要使用僅在單個 JVM 實例中使用的值。例如,代碼必須調用 JVM
運行時中的某些運行時常式來執行特定操作,如查找未解析的方法或分配內存。這些運行時常式的地址可能在每次將 JVM 載入到內存時變化。因此 AOT
編譯代碼需要綁定到 JVM 的當前執行環境中,然後才能執行。其他的例子有字元串的地址和常量池入口的內部位置。

在 WebSphere Real Time 中,AOT 本地代碼編譯通過 jxeinajar工具(參見圖 2)來執行。該工具對 JAR 文件中所有類的所有方法應用本地代碼編譯,也可以選擇性地對需要的方法應用本地代碼編譯。結果被存儲到名為 Java eXEcutable (JXE) 的內部格式中,但是也可輕松地存儲到任意的持久性容器中。

您可能認為對所有的代碼進行靜態編譯是最好的方法,因為可以在運行時執行最大數量的本地代碼。但是此處可以作出一些權衡。編譯的方法越多,代碼佔用的內存
就越多。編譯後的本地代碼大概比位元組碼大 10 倍:本地代碼本身的密度比位元組碼小,而且必須包含代碼的附加元數據,以便將代碼綁定到 JVM
中,並且在出現異常或請求堆棧跟蹤時正確執行代碼。構成普通 Java 應用程序的 JAR
文件通常包含許多很少執行的方法。編譯這些方法會消耗內存卻沒有什麼預期收益。相關的內存消耗包括以下過程:將代碼存儲到磁碟上、從磁碟取出代碼並裝入
JVM,以及將代碼綁定到 JVM。除非多次執行代碼,否則這些代價不能由本地代碼相對解釋的性能優勢來彌補。

圖 2. jxeinajar

跟大小問題相違背的一個事實是:在編譯過的方法和解釋過的方法之間進行的調用(即編譯過的方法調用解釋過的方法,或者相反)可能比這兩類方法各自內部之間
進行的調用所需的開銷大。動態編譯器通過最終編譯所有由 JIT
編譯代碼頻繁調用的那些解釋過的方法來減少這項開銷,但是如果不使用動態編譯器,則這項開銷就不可避免。因此如果是選擇性地編譯方法,則必須謹慎操作以使
從已編譯方法到未編譯方法的轉換最小化。為了在所有可能的執行中都避免這個問題而選擇正確的方法會非常困難。
優點
雖然 AOT 編譯代碼具有上述的缺點和挑戰,但是提前編譯 Java 程序可以提高性能,尤其是在不能將動態編譯器作為有效解決方案的環境中。

可以通過謹慎地使用 AOT 編譯代碼加快應用程序啟動,因為雖然這種代碼通常比 JIT
編譯代碼慢,但是卻比解釋代碼快很多倍。此外,因為載入和綁定 AOT
編譯代碼的時間通常比檢測和動態編譯一個重要方法的時間少,所以能夠在程序執行的早期達到那樣的性能。類似地,互動式應用程序可以很快地從本地代碼中獲
益,無需使用引起較差響應能力的動態編譯。

RT 應用程序也能從 AOT 編譯代碼中獲得重要的收益:更具確定性的性能超過了解釋的性能。WebSphere Real Time
使用的動態 JIT 編譯器針對在 RT 系統中的使用進行了專門的調整。使編譯線程以低於 RT
任務的優先順序操作,並且作出了調整以避免生成帶有嚴重的不確定性性能影響的代碼。但是,在一些 RT 環境中,出現 JIT
編譯器是不可接受的。此類環境通常需要最嚴格的時限管理控制。在這些例子中,AOT
編譯代碼可以提供比解釋過的代碼更好的原始性能,又不會影響現有的確定性。消除 JIT
編譯線程甚至消除了啟動更高優先順序 RT 任務時發生的線程搶占所帶來的性能影響。

優缺點統計

動態(JIT)編譯器支持平台中立性,並通過利用應用程序執行的動態行為和關於載入的類及其層次結構的信息來生成高質量的代碼。但是
JIT
編譯器具有一個有限的編譯時預算,而且會影響程序的運行時性能。另一方面,靜態(AOT)編譯器則犧牲了平台無關性和代碼質量,因為它們不能利用程序的動
態行為,也不具有關於載入的類或類層次結構的信息。AOT 編譯擁有有效無限制的編譯時預算,因為 AOT
編譯時間不會影響運行時性能,但是在實踐中開發人員不會長期等待靜態編譯步驟的完成。

表 1 總結了本文討論的 Java 語言動態和靜態編譯器的一些特性:

表 1. 比較編譯技術

兩種技術都需要謹慎選擇編譯的方法以實現最高的性能。對動態編譯器而言,編譯器自身作出決策,而對於靜態編譯器,由開發人員作出選擇。讓
JIT 編譯器選擇編譯的方法是不是優點很難說,取決於編譯器在給定情形中推斷能力的好壞。在大多數情況下,我們認為這是一種優點。

因為它們可以最好地優化運行中的程序,所以 JIT 編譯器在提供穩定狀態性能方面更勝一籌,而這一點在大量的生產 Java
系統中最為重要。靜態編譯可以產生最佳的互動式性能,因為沒有運行時編譯行為來影響用戶預期的響應時間。通過調整動態編譯器可以在某種程度上解決啟動和確
定性性能問題,但是靜態編譯在需要時可提供最快的啟動速度和最高級別的確定性。表 2 在四種不同的執行環境中對這兩種編譯技術進行了比較:

表 2. 使用這些技術的最佳環境

圖 3 展示了啟動性能和穩定狀態性能的總體趨勢:

圖 3. AOT 和 JIT 的性能對比

使用 JIT 編譯器的初始階段性能很低,因為要首先解釋方法。隨著編譯方法的增多及 JIT
執行編譯所需時間的縮短,性能曲線逐漸升高最後達到性能峰值。另一方面,AOT 編譯代碼啟動時的性能比解釋的性能高很多,但是無法達到 JIT
編譯器所能達到的最高性能。將靜態代碼綁定到 JVM 實例中會產生一些開銷,因此開始時的性能比穩定狀態的性能值低,但是能夠比使用 JIT
編譯器更快地達到穩定狀態的性能水平。

沒有一種本地代碼編譯技術能夠適合所有的 Java
執行環境。某種技術所擅長的通常正是其他技術的弱項。出於這個原因,需要同時使用這兩種編譯技術以滿足 Java
應用程序開發人員的要求。事實上,可以結合使用靜態和動態編譯以便提供最大可能的性能提升 —— 但是必須具備平台無關性,它是 Java
語言的主要賣點,因此不成問題。

結束語

本文探討了 Java 語言本地代碼編譯的問題,主要介紹了 JIT 編譯器形式的動態編譯和靜態 AOT 編譯,比較了二者的優缺點。

雖然動態編譯器在過去的十年裡實現了極大的成熟,使大量的各種 Java 應用程序可以趕上或超過靜態編譯語言(如 C++ 或
Fortran)所能夠達到的性能。但是動態編譯在某些類型的應用程序和執行環境中仍然不太合適。雖然 AOT
編譯號稱動態編譯缺點的萬能解決方案,但是由於 Java 語言本身的動態特性,它也面臨著提供本地編譯全部潛能的挑戰。

這兩種技術都不能解決 Java 執行環境中本地代碼編譯的所有需求,但是反過來又可以在最有效的地方作為工具使用。這兩種技術可以相互補充。能夠恰當地使用這兩種編譯模型的運行時系統可以使很大范圍內的應用程序開發環境中的開發人員和用戶受益。

3. 編譯告警被關閉的參數

javac參數 編譯警告關閉_JVM之JIT即時編譯 原創
2020-12-26 08:21:34

水天姬

碼齡4年

關注


當今Java語言被廣為接受的優點之一就有即時編譯,即時編譯的存在使得Java應用可以運行時間的增長而獲得更高的性能。

如果有對jvm做過研究的朋友,一定聽說過這樣一段話:由於即時編譯技術的進步,尤其是逃逸分析技術的日漸強大,棧上分配、標量替換優化手段已經導致一些微妙的變化悄然發生,Java對象實例都分配在堆上也變得不那麼絕對了。虛擬機發展到今天,即時編譯也是我們對jvm討論的話題之一,今天我也來對即時編譯進行一個詳述。
JIT(just-in-time)

Coding的嗶嗶叨叨

JIT即just-in-time的縮寫,即時編譯技術,可以加快java的執行速度。我們都知道,從源代碼,經javac編譯生成.class文件位元組碼,在經過類載入器,載入到內存,jvm通過解釋位元組碼翻譯成對應的機器指令,逐條讀入,逐條翻譯解釋,這就是 jvm解釋執行的過程。
很顯然,解釋執行,其執行速度必然會比可執行的二進制位元組碼程序慢很多,為了提高執行速度,引入了即時編譯技術。

Java程序最初是通過解釋器(interpreter)進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁,就會把這些代碼認為是「熱點代碼」。為了提高熱點代碼的執行效率,在運行時,虛擬機會把這些代碼編譯成與本地機器相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱之為「即時編譯器(just-in-time compiler)」。

注意:即時編譯器並不是java虛擬機的必須部分,java虛擬機規范並沒有規定必須要有即時編譯器的存在,並且,更沒有要求或指導如何實現即時編譯器;但是即時編譯器的好壞、代碼優化程度高低卻是衡量一款商用虛擬機的重要指標之一,它也是虛擬機最核心且最能體現虛擬機技術水平的的部分。

由於上述原因,所以即時編譯器完全是某虛擬機具體實現的相關內容,所以,如無特殊說明,本文所提到的即時編譯器都是Hotspot虛擬機內的即時編譯器;所提到的虛擬機也是專門特指Hotspot虛擬機。

JVM運行原理

Coding的嗶嗶叨叨

這兒我們貼出一張圖,大家結合著圖進行理解即可。




為什麼Hotspot要使用解釋器與編譯器並存的架構?

Coding的嗶嗶叨叨

首先要說明,並不是所有的虛擬機實現都採用解釋器與編譯器並行的方案,但許多的主流虛擬機,如Hotspot,都同時包含解釋器和編譯器。解釋器與編譯器各有優勢: 當程序需要快速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行;當程序運行環境內存資源限制較大(如嵌入式系統),可以使用解釋器節約內存。反之可以使用編譯器提高執行效率,此外,編譯後如果出現「罕見陷阱」,可以逆優化退回到解釋執行。


編譯的時間開銷

Coding的嗶嗶叨叨

我們先抽象看下解釋器和編譯器的執行過程。
解釋器:輸入代碼->[解釋器解釋執行]->執行結果

編譯器:輸入代碼->[編譯器編譯]->編譯後的代碼->執行結果。

從上面的抽象執行過程來看,JIT編譯執行,要比解釋執行慢,多了一個執行編譯後代碼的過程,所以針對「只執行一次的代碼」,解釋執行總要比編譯執行快。而我們通常所說的,JIT編譯執行,比解釋執行快,並不是說「編譯」這個動作比「解釋」這個動作快,而是在非「只執行一次的代碼」環境下,JIT編譯執行要比解釋執行快,要注意這個前提環境,而我們所指的不止執行一次的代碼,有以下兩個場景:
方法被多次調用,相反類似類構造器(class initializer,())只被調用一次。

存在循環。

只有頻繁執行的代碼,才會保證JIT編譯執行的正向收益。
編譯的空間開銷

Coding的嗶嗶叨叨

對java語言來說,編譯後的代碼相對於class位元組碼,膨脹達10倍是很正常的,同上面所說的時間開銷一樣,只有針對頻繁執行的代碼,才能保證有正向收益,才值得去編譯,如果把所有的代碼都進行編譯,會很明顯的增加代碼所佔空間。

JIT編譯,會將編譯後的匯編指令保存在代碼緩存中,代碼緩存是固定大小的,當jvm代碼緩存一旦被填滿,jvm就不能編譯更多的代碼。

所以,這也解釋了,為什麼虛擬機不完全採用JIT編譯執行,而是選擇解釋器與編譯器混合共存的執行引擎。

現在並沒有一個好的機制可以確定一個特定的應用到底需要多大的代碼緩存。因此,當需要提高代碼緩存時,這將是一種湊巧的操作,一個通常的做法是將代碼緩存變成默認大小的兩倍或四倍。

可以通過 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默認大小)來最大化代碼緩存大小。代碼緩存的管理類似於 JVM 中的內存管理:有一個初始大小(用-XX:InitialCodeCacheSize=N 來聲明)。代碼緩存的大小從初始大小開始,隨著緩存被填滿而逐漸擴大。代碼緩存的初始大小是基於晶元架構(例如 Intel 系列機器,client 編譯器模式下代碼緩存大小起始於 160KB,server 編譯器模式下代碼緩存大小則起始於 2496KB)以及使用的編譯器的。重定義代碼緩存的大小並不會真正影響性能,所以設置 ReservedCodeCacheSize 的大小一般是必要的。

再者,如果 JVM 是 32 位的,那麼運行過程大小不能超過 4GB。這包括了 Java 堆,JVM 自身所有的代碼空間(包括其本身的庫和線程棧),應用程序分配的任何的本地內存,當然還有代碼緩存。

所以說代碼緩存並不是無限的,很多時候需要為大型應用程序來調優(或者甚至是使用分層編譯的中型應用程序)。比如 64 位機器,為代碼緩存設置一個很大的值並不會對應用程序本身造成影響,應用程序並不會內存溢出,這些額外的內存預定一般都是被操作系統所接受的。

Hotspot的兩個不同編譯器

Coding的嗶嗶叨叨

Hotspot內置了兩個解釋編譯器: client compiler(c1)和server compiler(c2),分別用在客戶端和服務端。HotSpot虛擬機會根據自身版本與宿主機器的硬體性能自動選擇運行模式,用戶也可以使用「-client」或「-server」參數去強制指定虛擬機運行在Client模式或Server模式。c1編譯器有更快的編譯速度,c2編譯器有更高的編譯質量。
哪些代碼會被即時編譯?

Coding的嗶嗶叨叨

哪些代碼會被稱為熱點代碼,其實我們在上面也提過過,以下兩種場景:
被多次調用的方法。

被多次執行的循環體。

注意:上面兩種情況,編譯器都是以整個方法作為編譯對象,編譯方法發生在方法執行過程中,我們稱之為:棧上替換(OSR,On Stack Replacement),方法棧針還在棧上,方法就被替換了。

如何編譯為本地代碼?

Coding的嗶嗶叨叨

Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。 對Client Compiler來說,它是一個簡單快速的編譯器,主要關注點在於局部優化,而放棄許多耗時較長的全局優化手段。 而Server Compiler則是專門面向伺服器端的,並為服務端的性能配置特別調整過的編譯器,是一個充分優化過的高級編譯器。
HotSpot的熱點探測技術

Coding的嗶嗶叨叨

目前主流的熱點探測技術有兩種:
基於采樣的熱點探測

從字面也能基本理解,這種方法基本是周期性的檢查各個線程的棧頂,如果發現某個方法經常性的出現在棧頂,那麼這個方法就是「熱點代碼」。這種方式簡單有效,但是也存在很明顯的問題,很難精準的確認一個方法是熱點方法,容易因為線程阻塞或者其他的一些外界因素的影響而擾亂熱點探測。
基於計數器的熱點探測

採用這種方式的虛擬機會為每個方法(甚至是代碼)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值,就認為是「熱點方法」。這種方式實現起來就復雜了,需要為每個方法建立並維護計數器,而且不能直接獲取方法的調用關系,但是這種方式的統計結果就相對精確嚴謹。 Hotspot虛擬機採用的是計數器熱點探測方式。 ,為每個方法都維護了兩個計數器:方法調用計數器和回邊計數器。在確定虛擬機參數的前提下,這兩個計數器都有一個確定的閾值,當計數器超過閾值溢出了,就會觸發JIT編譯。
方法調用計數器
顧名思義,這個計數器用於統計方法被調用的次數。當一個方法被調用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優先使用編譯後的本地代碼來執行。如果不存在已被編譯過的版本,則將此方法的調用計數器值加1,然後判斷方法調用計數器與回邊計數器值之和是否超過方法調用計數器的閾值。如果超過閾值,那麼將會向即時編譯器提交一個該方法的代碼編譯請求。如果不做任何設置,執行引擎並不會同步等待編譯請求完成,而是繼續進行解釋器按照解釋方式執行位元組碼,直到提交的請求被編譯器編譯完成。當編譯工作完成之後,這個方法的調用入口地址就會系統自動改寫成新的,下一次調用該方法時就會使用已編譯的版本。



回邊計數器
它的作用就是統計一個方法中循環體代碼執行的次數,在位元組碼中遇到控制流向後跳轉的指令稱為「回邊」。

JIT的知識點,大部分應該都提到了,還有啥沒提到的,留言,補充。
文章部分圖片來源於網路其他優秀工程師的分享。
最後還想給大家補充個知識點,關於java的javac編譯和即時編譯。在剛開始接觸到java的時候,每次談到編譯,總是搞不清到底說的是那種編譯,在這也簡單聊一下,給大家做個簡單的區分。
javac編譯與JIT編譯

Coding的嗶嗶叨叨

我們通常所說的編譯,有兩種情形,一種是從源代碼(.java)編譯為位元組碼(.class);一種是虛擬機執行位元組碼的過程,從位元組碼編譯為本地機器語言。所以在提到編譯的時候,一定要區分清是哪種(剛開始結束java的時候被這個搞得暈乎的很久)。我們一般把javac的編譯過程稱之為:前端編譯;把JIT編譯稱之為後端編譯。如果可能,下篇文章,我把java的前端編譯過程進行一下說明。
不積跬步,無以至千里。

文章有幫助的話,點個轉發、在看唄。

謝謝支持喲 (*^__^*)

END

?



相關資源:java編譯器源碼解析-warnings-ng-plugin:Jenkins警告插件-下一代

4. 請問CPU JIT什麼意思

JIT Compiler(Just-in-time Compiler) 就是即時編譯。

當Java執行runtime環境時,每遇到一個新的類別(class:類別是Java程式中的功能群組),JIT編譯器在此時就會針對這個類別進行編譯(compile)作業。經過編譯後的程式,被優化成相當精簡的原生型指令碼(native code),這種程式的執行速度相當快。花費少許的編譯時間來節省稍後相當長的執行時間,JIT這種設計的確增加不少效率,但是它並未達到最頂尖的效能,因為某些極少執行到的Java指令在編譯時所額外花費的時間可能比轉譯器在執行時的時間還長,針對這些指令而言,整體花費的時間並沒有減少,所以開啟後能提高cpu的性能。

5. java語言在被設計的時候為什麼會加上 編譯 這一步

java語言在被設計的時候為什麼會加上編譯這一步,其實主要的考慮因素是性能,靜態語言的執行效率比動態語言要高得多。所以需要編譯。

6. 應用編譯優化哪個模式好

即時編譯技術。
JIT為「即時編譯技術」,當App運行時,每當遇到一個新類,JIT編譯器就會對這個類進行編譯,經過編譯後的代碼,會被優化成相當精簡的原生型指令碼(即nativecode),這樣在下次執行到相同邏輯的時候,速度就會更快。

7. 什麼是Just In Time編譯器

JIT(just-in-time compilation)指計算機領域里,即時編譯也被成枝殲為動態翻譯,是一種通過在運行時將位元組碼翻譯為機器碼,從而改善位元組碼編譯語言性能的技術。即時編譯前期的兩個運行時理論是位元組碼編譯和動態編譯。

在編譯為位元組碼的系統如 Limb 編程語言,Smalltalk, UCSD P-System, Perl, GNU CLISP, 和 Java 的早期版本中, 源代碼被翻譯為一種中間表示即位元組碼。 位元組碼不是任何特定計算機的機器碼, 它可以在多種計算機體系中移植。位元組碼被解釋著運行在虛擬機里。

動態編譯環境是一種在執行瞎搭鎮時使用編譯器的編譯環境。 例如, 多數 Common Lisp 系統有一個編譯函數,他可以編譯在運行時創建的函數。

在即時編譯環境下, 位元組碼的編譯是第一步, 它將源代碼遞歸到可移植和可優化的中間表示。位元組碼被部署到目標系統。 當執行代碼時,運行時環境的編譯器將位元組碼翻譯為本地機器碼。 基於每個文件或每個函數:函數僅僅在他們要被執行時才會被編譯。

目標是要組合利用本地和位元組碼編譯的多種優勢:多數重量級的任務如源代碼解析和基本性能的優化在編譯時處理,將位元組碼編譯為機器碼比起從源代碼編譯為機器碼要快得多。部署位元組碼是可移植的,而機器碼只限於特定的系統結構。從位元組碼到機器碼編譯器的實現更容易,因為大部分工作已經在實現位元組碼編磨粗譯器時完成。

8. jit是什麼含義

jit的含義是准時制生產方式。

准時制生產方式(Just In Time簡稱JIT),又稱作無庫存生產方式(stockless proction),零庫存(zero inventories),一個流(one-piece flow)或者超級市場生產方式(supermarket proction)。

准時制是日本豐田汽車公司在20世紀60年代實行的一種生產方式,1973年以後,這種方式對豐田公司度過第一次能源危機起到了突出的作用。

後引起其它國家生產企業的重視,並逐漸在歐洲和美國的日資企業及當地企業中推行開來,這一方式與源自日本的其它生產、流通方式一起被西方企業稱為「日本化模式」。



(8)jit即時編譯器作用擴展閱讀:

應用領域

物料需求計劃是通過把生產環節與對物料的需求計劃緊密地聯在一起,由此來降低需求的不確定性,從而降低庫存水平。而准時制運作強調對全部運作環節進行組織,使得各個運作環節在最恰源賣當的時間實施運作。坦睜

看起來,定義是非常簡單的,但是運作起來的確不是一件輕松的事情。要想取得成功,企業需要對其內部的態度和運作模式完全加以改變,同時還需要經過周密的計劃和嚴密控制、實施工作才行,這往往需要花費數年的時間。

准時制運作的理念由來已久,20世紀20年代的福特汽車已經開始了這方面的運作。而關於准時制運作革新的高潮從20世紀70年代開始,日本的豐田公司和歐洲的施樂公司就是最早實施准時制運作的公司之一,其中日本豐田公司在這方面是久負盛名。

對於准時制運作這一模式,人們通常冠以多種名稱,如零庫存、精益生產、豐田系統模式、連續流生產械等。


9. jit的含義是什麼

jit是just in time的縮寫,常用的有兩個領域,一個是工業中的准時生產系統,一個編程中即時編譯編譯器的縮寫。

准時生產方法是指生產的產品能夠精準地滿足客戶在時間、質量和數量上的需求,無論客戶是產品的最終用戶還是處於生產線上的其他流程。採用JIT時,配送到生產現場的部件和材料正如生產所需,企業不會為防止發生配送延遲的情況而儲備材料和部件。

工業准時生產方法的核心

准時生產方法就是強調企業生產產品的時候,應該完全按照客戶的需求來生產,客戶什麼時間需要,就什麼時間生產出來,客戶需要什麼品種,就生產什麼品種,客戶需要多大量,就生產多大量,如果真正能做到這種程度,意味著企業生產出的產品馬上就可以轉移出去。

所以這種方法的核心就是按需生產,如果能實現按需生產,伴隨著產品及時地轉移,企業的存貨庫存就會不斷降低,伴隨著存貨的不斷降低,企業有關的成本也就會不斷降低。

閱讀全文

與jit即時編譯器作用相關的資料

熱點內容
程序員考注冊會計師 瀏覽:955
怎麼使用access的命令按鈕 瀏覽:897
有點錢app在哪裡下載 瀏覽:832
博途v15解壓後無法安裝 瀏覽:203
什麼是根伺服器主機 瀏覽:436
安卓手游怎麼申請退款 瀏覽:553
安卓系統如何分享網頁 瀏覽:278
ad如何編譯pcb工程 瀏覽:412
除了滴滴app哪裡還能用滴滴 瀏覽:399
截圖怎麼保存文件夾然後壓縮 瀏覽:8
幻影伺服器怎麼樣 瀏覽:27
具體哪些廣東公司招程序員 瀏覽:870
嵌入式編譯器教程 瀏覽:306
ssl數據加密傳輸 瀏覽:86
51單片機定時器方式2 瀏覽:331
命令行查看開機時間 瀏覽:813
python微博復雜網路分析 瀏覽:550
rf3148編程器 瀏覽:505
浙江標准網路伺服器機櫃雲主機 瀏覽:589
設置網路的伺服器地址 瀏覽:601