您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何理解JVM中的JIT即時編譯及優化技術,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
通過 java -version 可查看 JVM 所處的模式,并可以通過修改配置文件進行配置,那它們有什么區別呢?
Server:-Server 模式啟動時,速度較慢,但是啟動之后,性能更高,適合運行服務器后臺程序
Client:-Client 模式啟動時,速度較快,啟動之后不如 Server,適合用于桌面等有界面的程序
理解
當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”。
熱點代碼的分類
被多次調用的方法
一個方法被調用得多了,方法體內代碼執行的次數自然就多,成為“熱點代碼”是理所當然的。
被多次執行的循環體
一個方法只被調用過一次或少量的幾次,但是方法體內部存在循環次數較多的循環體,這樣循環體的代碼也被重復執行多次,因此這些代碼也應該認為是“熱點代碼”。
上面提到的多次是一個不具體的詞語,那到底是多少次才能成為熱點代碼呢?
如何檢測熱點代碼
判斷一段代碼是否是熱點代碼,是否需要觸發即使編譯,這樣的行為稱為熱點探測,熱點探測并不一定知道方法具體被調用了多少次,目前主要的熱點探測判定方式有兩種:
基于采樣的熱點探測:采用這種方法的虛擬機會周期性地檢查各個線程的棧頂如果發現某個(或某些)方法經常出現在棧頂,那這個方法就是“熱點方法”
優點:實現簡單高效,容易獲取方法調用關系(將調用堆棧展開即可)
缺點:不精確,容易因為因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測
基于計數器的熱點探測:采用這種方法的虛擬機會為每個方法(甚至是代碼塊)建立計數器,統計方法的執行次數,如果次數超過一定的閾值就認為它是“熱點方法”
優點:統計結果精確嚴謹
缺點:實現麻煩,需要為每個方法建立并維護計數器,不能直接獲取到方法的調用關系
HotSpot使用第二種 - 基于計數器的熱點探測方法。
確定了檢測熱點代碼的方式,如何計算具體的次數呢?
計數器的種類(兩種共同協作)
方法調用計數器:這個計數器用于統計方法被調用的次數。默認閾值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次
回邊計數器:統計一個方法中循環體代碼執行的次數
了解了熱點代碼和計數器有什么用呢?達到計數器的閾值會觸發后文講解的即時編譯,也就是說即時編譯是需要達到某種條件才會觸發的,先寫結論,后文講解什么是即時編譯器。
兩個計數器的協作(這里討論的是方法調用計數器的情況):當一個方法被調用時,會先檢查該方法是否存在被 JIT(后文講解) 編譯過的版本,如果存在,則優先使用編譯后的本地代碼來執行。如果不存在已被編譯過的版本,則將此方法的調用計數器加 1,然后判斷方法調用計數器與回邊計數器之和是否超過方法調用計數器的閾值。如果已經超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求。
當編譯工作完成之后,這個方法的調用入口地址就會被系統自動改成新的,下一次調用該方法時就會使用已編譯的版本。
字節碼是指平常所了解的 .class 文件,Java 代碼通過 javac 命令編譯成字節碼
機器碼和本地代碼都是指機器可以直接識別運行的代碼,也就是機器指令
字節碼是不能直接運行的,需要經過 JVM 解釋或編譯成機器碼才能運行
此時你要問了,為什么 Java 不直接編譯成機器碼,這樣不是更快嗎?
1. 機器碼是與平臺相關的,也就是操作系統相關,不同操作系統能識別的機器碼不同,如果編譯成機器碼那豈不是和 C、C++差不多了,不能跨平臺,Java 就沒有那響亮的口號 “一次編譯,到處運行”;
2.之所以不一次性全部編譯,是因為有一些代碼只運行一次,沒必要編譯,直接解釋運行就可以。而那些“熱點”代碼,反復解釋執行肯定很慢,JVM在運行程序的過程中不斷優化,用JIT編譯器編譯那些熱點代碼,讓他們不用每次都逐句解釋執行;
3.還有一方面的原因是后文講解的解釋器與編譯器共存的原因。
為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器
編譯器:把源程序的每一條語句都編譯成機器語言,并保存成二進制文件,這樣運行時計算機可以直接以機器語言來運行此程序,速度很快;
解釋器:只在執行程序時,才一條一條的解釋成機器語言給計算機來執行,所以運行速度是不如編譯后的程序運行的快的;
通過javac
命令將 Java 程序的源代碼編譯成 Java 字節碼,即我們常說的 class 文件。這是我們通常意義上理解的編譯。
字節碼并不是機器語言,要想讓機器能夠執行,還需要把字節碼翻譯成機器指令。這個過程是Java 虛擬機做的,這個過程也叫編譯。是更深層次的編譯。(實際上就是解釋,引入 JIT 之后也存在編譯)
此時又有疑惑了,Java不是解釋執行的嗎?
沒錯,Java 需要將字節碼逐條翻譯成對應的機器指令并且執行,這就是傳統的 JVM 的解釋器的功能,正是由于解釋器逐條翻譯并執行這個過程的效率低,引入了 JIT 即時編譯技術。
必須指出的是,不管是解釋執行,還是編譯執行,最終執行的代碼單元都是可直接在真實機器上運行的機器碼,或稱為本地代碼
附一張圖來理解
解釋器與編譯器兩者各有優勢
解釋器:當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。
編譯器:在程序運行后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取更高的執行效率。
兩者的協作:在程序運行環境中內存資源限制較大時,可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。當通過編譯器優化時,發現并沒有起到優化作用,,可以通過逆優化退回到解釋狀態繼續執行。
即時編譯器并不是虛擬機必需的部分,Java 虛擬機規范并沒有規定 Java 虛擬機內必須要有即時編譯器的存在,更沒有限定或指導即時編譯器應該如何去實現。
但是,即時編譯器編譯性能的好壞、代碼優化程度的高低卻是衡量一款商用虛擬機優秀與否的最關鍵的指標之一。它也是虛擬機中最核心且最能體現虛擬機技術水平的部分。
Client Compiler - C1編譯器
Server Compiler - C2編譯器
目前主流的 HotSpot 虛擬機(JDK1.7 及之前版本的虛擬機)默認采用一個解釋器和其中一個編譯器直接配合的方式工作,程序使用哪個編譯器,取決于虛擬機運行的模式,就是文章開頭提到的兩種模式。
在 HotSpot 中,解釋器和 JIT 即時編譯器是同時存在的,他們是 JVM 的兩個組件。對于不同類型的應用程序,用戶可以根據自身的特點和需求,靈活選擇是基于解釋器運行還是基于 JIT 編譯器運行。HotSpot 為用戶提供了幾種運行模式供選擇,可通過參數設定,分別為:解釋模式、編譯模式、混合模式,HotSpot 默認是混合模式,需要注意的是編譯模式并不是完全通過 JIT 進行編譯,只是優先采用編譯方式執行程序,但是解釋器仍然要在編譯無法進行的情況下介入執行過程。
產生的原因:由于即時編譯器編譯本地代碼需要占用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間可能更長;而且要想編譯出優化程度更高的代碼,解釋器可能還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響。為了在程序啟動響應速度與運行效率之間達到最佳平衡,HotSpot 虛擬機啟用分層編譯的策略
分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次:
第 0 層:程序解釋執行,解釋器不開啟性能監控功能,可觸發第 1 層編譯。
第 1 層:也稱為 C1 編譯,將字節碼編譯為本地代碼,進行簡單,可靠的優化,如有必要將加入性能監控的邏輯。
第 2 層(或 2 層以上):也稱為 C2 編譯,也是將字節碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
實施分層編譯后,Client Compiler 和 Server Compiler 將會同時工作,許多代碼都可能會被多次編譯看,用 Client Compiler 獲取更高的編譯速度,用 Server Compiler 獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集性能監控信息的任務。
Java 程序員有一個共識,以編譯方式執行本地代碼比解釋執行方式更快,之所以有這樣的共識,除去虛擬機解釋執行字節碼時額外消耗時間的原因外,還有一個重要的原因就是虛擬機設計團隊幾乎把對代碼的所有優化措施都集中在了即時編譯器中,因此一般來說,即時編譯器產生的本地代碼會比 javac 產生的字節碼更優秀。以下是具有代表性的 HotSpot 虛擬機的即時編譯器在生成代碼時采用的代碼優化技術:
語言無關的經典優化技術之一:公共子表達式消除
如果一個表達式 E 已經計算過了,并且從先前的計算到現在 E 中所有變量的值都沒有發生變化,那么 E 的這次出現就成為了公共子表達式。對于這種表達式,沒必要花時間再對它進行計算,只需要直接使用前面計算過的表達式結果代替 E 就可以了。
例子:int d = (c*b) *12 + a + (a+ b*c) -> int d = E *12 + a + (a+ E)
語言相關的經典優化技術之一:數組范圍檢查消除
在 Java 語言中訪問數組元素的時候系統將會自動進行上下界的范圍檢查,超出邊界會拋出異常。對于虛擬機的執行子系統來說,每次數組元素的讀寫都帶有一次隱含的條件判定操作,對于擁有大量數組訪問的程序代碼,這無疑是一種性能負擔。Java 在編譯期根據數據流分析可以判定范圍進而消除上下界檢查,節省多次的條件判斷操作。
最重要的優化技術之一:方法內聯
簡單的理解為把目標方法的代碼“復制”到發起調用的方法中,消除一些無用的代碼。只是實際的 JVM 中的內聯過程很復雜,在此不分析。
最前沿的優化技術之一:逃逸分析
逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中杯定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,稱為方法逃逸。甚至可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。
如果能證明一個對象不會逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個對象,則可以為這個變量進行一些高效的優化:
棧上分配:將不會逃逸的局部對象分配到棧上,那對象就會隨著方法的結束而自動銷毀,減少垃圾收集系統的壓力。
同步消除:如果該變量不會發生線程逃逸,也就是無法被其他線程訪問,那么對這個變量的讀寫就不存在競爭,可以將同步措施消除掉(同步是需要付出代價的)
標量替換:標量是指無法在分解的數據類型,比如原始數據類型以及reference類型。而聚合量就是可繼續分解的,比如 Java 中的對象。標量替換如果一個對象不會被外部訪問,并且對象可以被拆散的話,真正執行時可能不創建這個對象,而是直接創建它的若干個被這個方法使用到的成員變量來代替。這種方式不僅可以讓對象的成員變量在棧上分配和讀寫,還可以為后后續進一步的優化手段創建條件。
關于如何理解JVM中的JIT即時編譯及優化技術就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。