java解釋執行後是否常駐內存?為何需要JIT技術?

融化的雪人


什麼是 JIT ?

為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器

什麼是編譯和解釋?

編譯器:把源程序的每一條語句都編譯成機器語言,並保存成二進制文件,這樣運行時計算機可以直接以機器語言來運行此程序,速度很快;

解釋器:只在執行程序時,才一條一條的解釋成機器語言給計算機來執行,所以運行速度是不如編譯後的程序運行的快的;

通過命令將 Java 程序的源代碼編譯成 Java 字節碼,即我們常說的 class 文件。這是我們通常意義上理解的編譯。

字節碼並不是機器語言,要想讓機器能夠執行,還需要把字節碼翻譯成機器指令。這個過程是Java 虛擬機做的,這個過程也叫編譯。是更深層次的編譯。(實際上就是解釋,引入 JIT 之後也存在編譯)

此時又有疑惑了,Java不是解釋執行的嗎?

沒錯,Java 需要將字節碼逐條翻譯成對應的機器指令並且執行,這就是傳統的 JVM 的解釋器的功能,正是由於解釋器逐條翻譯並執行這個過程的效率低,引入了 JIT 即時編譯技術。

必須指出的是,不管是解釋執行,還是編譯執行,最終執行的代碼單元都是可直接在真實機器上運行的機器碼,或稱為本地代碼

附一張圖來理解

image

編譯原理參考:深入分析Java的編譯原理

為何 HotSpot 虛擬機要使用解釋器與編譯器並存的架構?

解釋器與編譯器兩者各有優勢

解釋器:當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。

編譯器:在程序運行後,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲取更高的執行效率。

兩者的協作:在程序運行環境中內存資源限制較大時,可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。當通過編譯器優化時,發現並沒有起到優化作用,,可以通過逆優化退回到解釋狀態繼續執行。

即時編譯器與 Java 虛擬機的關係

即時編譯器並不是虛擬機必需的部分,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 中的對象。標量替換如果一個對象不會被外部訪問,並且對象可以被拆散的話,真正執行時可能不創建這個對象,而是直接創建它的若干個被這個方法使用到的成員變量來代替。這種方式不僅可以讓對象的成員變量在棧上分配和讀寫,還可以為後後續進一步的優化手段創建條件。

按自己理解整理的,知識點順序不知是否合適,還請大家指導。


鮮事


也不是Java程序員,簡單談談我的看法。

1,一般意義上的垃圾回收是針對對象實例,而非類型本身,要回收類型,需要從 Classloader 入手;

2,Java是編譯型語言,但不是原生編譯,編譯結果是中間代碼(字節碼),這就是能跨平臺的原因,因此程序運行時需要從中間代碼轉換為機器碼;

3,將中間代碼編譯成機器碼有時間開銷,而且和中間代碼的量是成正比的,就是說要編譯的越多,花費的時間就越多,程序的啟動速度就越慢; 這也是所有使用中間語言(如Java、C#等)開發的程序,啟動速度明顯比原生編譯型程序要慢的原因;

4,JIT的作用是按需編譯,用到才編譯,編譯後緩存,可以提高程序的加載速度,效果立竿見影;

某屌炸天的編譯器,就是在中間代碼的編譯階段,直接編譯成機器碼,相當於原生編譯,這樣輸出的程序雖然加載和運行速度有所提高,但失去了跨平臺的能力。


熙爸愛釣魚


java編譯器,java解釋器

1.java程序是一種可跨平臺執行的語言,之所以可以跨平臺,是因為jvm的存在,JVM屏蔽了與具體操作系統平臺相關的信息,使java文件只需要生成為jvm可識別的字節碼(*.class)文件,jvm會將字節碼文件交給解釋器,翻譯成機器碼,由解釋器執行,JVM解釋執行字節碼文件就是JVM操作Java解釋器進行解釋執行字節碼文件的過程。這樣保證了在任何平臺上,都可以成功的執行java程序,jvm的存在就是java可跨平臺的核心。

2.解釋器是把高級語言一行一行直接翻譯再運行,它不會一次性把整個文件都翻譯過來,而是翻譯一句,執行一句,再翻譯,再執行,所以解釋器的程序運行起來會比較慢,每次都要解釋之後再執行。

3.動態編譯(dynamic compilation)指的是“在運行時進行編譯”;與之相對的是事前編譯(ahead-of-time compilation,簡稱AOT),也叫靜態編譯(static compilation)。

什麼是JIT

1.JIT編譯(just-in-time compilation)狹義來說是當某段代碼即將第一次被執行時進行編譯,因而叫“即時編譯”。JIT編譯是動態編譯的一種特例。JIT編譯一詞後來被泛華,時常與動態編譯等價;但要注意廣義與狹義的JIT編譯所指的區別。JIT(即時編譯)是用來提高java程序運行效率的,原本字節碼由解釋器需要經過解釋再運行,現在有了JIT技術,將字節碼編譯成平臺相關的原生機器碼,並進行各個層次的優化,這些機器碼會被緩存起來,以備下次使用,如果JIT對每條字節碼都進行編譯,緩存(緩存的指令是有限的),會增加開銷,因此JIT只對熱點代碼進行即時編譯,如循環,高頻度使用的方法,會將整個方法編譯成本地機器碼,然後直接運行機器碼。

java虛擬機並沒有規定一定要有JIT,但是,即時編譯器編譯性能的好壞、代碼優化程度的高低卻是衡量一款商用虛擬機優秀與否的最關鍵的指標之一,它也是虛擬機中最核心且最能體現虛擬機技術水平的部分。

由於Java虛擬機規範並沒有具體的約束規則去限制即使編譯器應該如何實現,所以這部分功能完全是與虛擬機具體實現相關的內容,如無特殊說明,我們提到的編譯器、即時編譯器都是指Hotspot虛擬機內的即時編譯器,虛擬機也是特指HotSpot虛擬機。

編譯過程

不論是物理機還是虛擬機,大部分的程序代碼從開始編譯到最終轉化成物理機的目標代碼或虛擬機能執行的指令集之前,都會按照如下圖所示的各個步驟進行:

其中綠色的模塊可以選擇性實現。很容易看出,上圖中間的那條分支是解釋執行的過程(即一條字節碼一條字節碼地解釋執行,如JavaScript),而下面的那條分支就是傳統編譯原理中從源代碼到目標機器代碼的生成過程。

如今,基於物理機、虛擬機等的語言,大多都遵循這種基於現代經典編譯原理的思路,在執行前先對程序源碼進行詞法解析和語法解析處理,把源碼轉化為抽象語法樹。對於一門具體語言的實現來說,詞法和語法分析乃至後面的優化器和目標代碼生成器都可以選擇獨立於執行引擎,形成一個完整意義的編譯器去實現,這類代表是C/C++語言。也可以把抽象語法樹或指令流之前的步驟實現一個半獨立的編譯器,這類代表是Java語言。又或者可以把這些步驟和執行引擎全部集中在一起實現,如大多數的JavaScript執行器。

Javac編譯

在Java中提到“編譯”,自然很容易想到Javac編譯器將*.java文件編譯成為*.class文件的過程,這裡的Javac編譯器稱為前端編譯器,其他的前端編譯器還有諸如Eclipse JDT中的增量式編譯器ECJ等。相對應的還有後端編譯器,它在程序運行期間將字節碼轉變成機器碼(現在的Java程序在運行時基本都是解釋執行加編譯執行),如HotSpot虛擬機自帶的JIT(Just In Time Compiler)編譯器(分Client端和Server端)。另外,有時候還有可能會碰到靜態提前編譯器(AOT,Ahead Of Time Compiler)直接把*.java文件編譯成本地機器代碼,如GCJ、Excelsior JET等,這類編譯器我們應該比較少遇到。

下面簡要說下Javac編譯(前端編譯)的過程。

詞法、語法分析

詞法分析是將源代碼的字符流轉變為標記(Token)集合。單個字符是程序編寫過程中的的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符等都可以成為標記,比如整型標誌int由三個字符構成,但是它只是一個標記,不可拆分。

語法分析是根據Token序列來構造抽象語法樹的過程。抽象語法樹是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表著程序代碼中的一個語法結構,如bao、類型、修飾符、運算符等。經過這個步驟後,編譯器就基本不會再對源碼文件進行操作了,後續的操作都建立在抽象語法樹之上。

填充符號表

完成了語法分析和詞法分析之後,下一步就是填充符號表的過程。符號表是由一組符號地址和符號信息構成的表格。符號表中所登記的信息在編譯的不同階段都要用到,在語義分析(後面的步驟)中,符號表所登記的內容將用於語義檢查和產生中間代碼,在目標代碼生成階段,黨對符號名進行地址分配時,符號表是地址分配的依據。

語義分析

語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是讀結構上正確的源程序進行上下文有關性質的審查。語義分析過程分為標註檢查和數據及控制流分析兩個步驟:

標註檢查步驟檢查的內容包括諸如變量使用前是否已被聲明、變量和賦值之間的數據類型是否匹配等。

數據及控制流分析是對程序上下文邏輯更進一步的驗證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。

字節碼生成

字節碼生成是Javac編譯過程的最後一個階段。字節碼生成階段不僅僅是把前面各個步驟所生成的信息轉化成字節碼寫到磁盤中,編譯器還進行了少量的代碼添加和轉換工作。 實例構造器<init>()方法和類構造器<clinit>()方法就是在這個階段添加到語法樹之中的(這裡的實例構造器並不是指默認的構造函數,而是指我們自己重載的構造函數,如果用戶代碼中沒有提供任何構造函數,那編譯器會自動添加一個沒有參數、訪問權限與當前類一致的默認構造函數,這個工作在填充符號表階段就已經完成了)。/<clinit>/<init>

JIT編譯

Java程序最初是僅僅通過解釋器解釋執行的,即對字節碼逐條解釋執行,這種方式的執行速度相對會比較慢,尤其當某個方法或代碼塊運行的特別頻繁時,這種方式的執行效率就顯得很低。於是後來在虛擬機中引入了JIT編譯器(即時編譯器),當虛擬機發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“Hot Spot Code”(熱點代碼),為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,完成這項任務的正是JIT編譯器。

現在主流的商用虛擬機(如Sun HotSpot、IBM J9)中幾乎都同時包含解釋器和編譯器(三大商用虛擬機之一的JRockit是個例外,它內部沒有解釋器,因此會有啟動相應時間長之類的缺點,但它主要是面向服務端的應用,這類應用一般不會重點關注啟動時間)。二者各有優勢:當程序需要迅速啟動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行;當程序運行後,隨著時間的推移,編譯器逐漸會返回作用,把越來越多的代碼編譯成本地代碼後,可以獲取更高的執行效率。解釋執行可以節約內存,而編譯執行可以提升效率。

HotSpot虛擬機中內置了兩個JIT編譯器:Client Complier和Server Complier,分別用在客戶端和服務端,目前主流的HotSpot虛擬機中默認是採用解釋器與其中一個編譯器直接配合的方式工作。

運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:

被多次調用的方法。

被多次調用的循環體。

兩種情況,編譯器都是以整個方法作為編譯對象,這種編譯也是虛擬機中標準的編譯方式。要知道一段代碼或方法是不是熱點代碼,是不是需要觸發即時編譯,需要進行Hot Spot Detection(熱點探測)。目前主要的熱點 判定方式有以下兩種:

基於採樣的熱點探測:採用這種方法的虛擬機會週期性地檢查各個線程的棧頂,如果發現某些方法經常出現在棧頂,那這段方法代碼就是“熱點代碼”。這種探測方法的好處是實現簡單高效,還可以很容易地獲取方法調用關係,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測。

基於計數器的熱點探測:採用這種方法的虛擬機會為每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,如果執行次數超過一定的閥值,就認為它是“熱點方法”。這種統計方法實現複雜一些,需要為每個方法建立並維護計數器,而且不能直接獲取到方法的調用關係,但是它的統計結果相對更加精確嚴謹。

在HotSpot虛擬機中使用的是第二種——基於計數器的熱點探測方法,因此它為每個方法準備了兩個計數器:方法調用計數器和回邊計數器。

方法調用計數器用來統計方法調用的次數,在默認設置下,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間內方法被調用的次數。

回邊計數器用於統計一個方法中循環體代碼執行的次數(準確地說,應該是回邊的次數,因為並非所有的循環都是回邊),在字節碼中遇到控制流向後跳轉的指令就稱為“回邊”。

在確定虛擬機運行參數的前提下,這兩個計數器都有一個確定的閥值,當計數器的值超過了閥值,就會觸發JIT編譯。觸發了JIT編譯後,在默認設置下,執行引擎並不會同步等待編譯請求完成,而是繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成為止(編譯工作在後臺線程中進行)。當編譯工作完成後,下一次調用該方法或代碼時,就會使用已編譯的版本。

由於方法計數器觸發即時編譯的過程與回邊計數器觸發即時編譯的過程類似,因此這裡僅給出方法調用計數器觸發即時編譯的流程:

Javac字節碼編譯器與虛擬機內的JIT編譯器的執行過程合起來其實就等同於一個傳統的編譯器所執行的編譯過程。


分享到:


相關文章: