聊聊JIT是如何影響JVM性能的

花半秒鐘就能看透事物本質的人,和花一生都看不清事物本質的人,註定是大相徑庭的命運 --教父

我的公衆號:月伴飛魚,歡迎關注java

以前說好的這期講解併發工具類,不過ReentrantLock源碼還沒肝完,理由嘛,太忙了,身體不舒服,腦殼沒貨,睡眠不足,劇還沒追完........面試

但說好的每週一篇乾貨,不能停,今天就先介紹一篇JVM相關知識數組

咱們知道Java虛擬機棧是線程私有的,每一個線程對應一個棧,每一個線程在執行一個方法時會建立一個對應的棧幀,棧幀負責存儲局部變量變量表、操做數棧、動態連接和方法返回地址等信息,每一個方法的調用過程,至關於棧幀在Java棧的入棧和出棧過程緩存

可是棧幀的建立是須要耗費資源的,尤爲是對於 Java 中常見的 getter、setter 方法來講,這些代碼一般只有一行,每次都建立棧幀的話就太浪費了。服務器

另外,Java 虛擬機棧對代碼的執行,採用的是字節碼解釋執行的方式,考慮到下面這段代碼,變量 a 聲明以後,就不再被使用,要是按照字節碼指令解釋執行的話,就要作不少無用功。併發

public class A{
    int attr = 0;
    public void test(){
        int a = attr;
        System.out.println("月伴飛魚");
    }
}

執行以下命令:工具

javap -v A

能夠看到這段代碼的字節碼指令性能

咱們可以看到 aload_0,getfield ,istore_1 這三個無用的字節碼指令操做。優化

aload_0 從局部變量0中裝載引用類型值,getfield 從對象中獲取字段,istore_1 將int類型值存入局部變量1

另外,咱們知道垃圾回收器回收的目標區域主要是堆,堆上建立的對象越多,GC 的壓力就越大。要是能把一些變量,直接在棧上分配,那 GC 的壓力就會小一些。ui

其實,咱們說的這幾個優化的可能性,JVM 已經經過JIT 編譯器(Just In Time Compiler)去作了,JIT 最主要的目標是把解釋執行變成編譯執行。

爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,這就是 JIT 編譯器的功能。

如上圖,JVM 會將調用次數很高,或者在 for 循環裏頻繁被使用的代碼,編譯成機器碼,而後緩存起來,下次調用相同方法的時候,就能夠直接使用。

那 JIT 編譯都有哪些手段呢?接下來咱們詳細介紹。

方法內聯

方法內聯它會把一些短小的方法體,直接歸入目標方法的做用範圍以內,就像是直接在代碼塊中追加代碼。這樣,就少了一次方法調用,執行速度就可以獲得提高,這就是方法內聯的概念。

可使用 -XX:-Inline 參數來禁用方法內聯,若是想要更細粒度的控制,可使用 CompileCommand 參數,例如:

-XX:CompileCommand=exclude,java/lang/String.indexOf

在 JDK 的源碼裏,也有不少被 @ForceInline註解的方法,這些方法,會在執行的時候被強制進行內聯;而被@DontInline註解的方法,則始終不會被內聯。

JIT 編譯以後的二進制代碼,是放在 Code Cache 區域裏的。這個區域的大小是固定的,並且一旦啓動沒法擴容。若是 Code Cache 滿了,JVM 並不會報錯,但會中止編譯。因此編譯執行就會退化爲解釋執行,性能就會下降。不只如此,JIT 編譯器會一直嘗試去優化你的代碼,形成 CPU 佔用上升。

經過參數 -XX:ReservedCodeCacheSize 能夠指定 Code Cache 區域的大小,若是你經過監控發現空間達到了上限,就要適當的增長它的大小。

分層編譯

HotSpot 虛擬機包含多個即時編譯器,有 C1,C2 和 Graal,JDK8 之後採用的是分層編譯的模式。

JMV使用額外線程進行即時編譯,能夠不用阻塞解釋執行的邏輯。JIT 一般會在觸發以後就在後臺運行,編譯完成以後就將相應的字節碼替換爲編譯後的代碼。

JIT 編譯方式有兩種:一種是編譯方法,另外一種是編譯循環。

具體介紹下幾個編譯器

C1 編譯器

C1 編譯器是一個簡單快速的編譯器,主要的關注點在於局部性的優化,適用於執行時間較短或對啓動性能有要求的程序,也稱爲Client Compiler,例如,GUI 應用對界面啓動速度就有必定要求。

C2 編譯器

C2 編譯器是爲長期運行的服務器端應用程序作性能調優的編譯器,適用於執行時間較長或對峯值性能有要求的程序,也稱爲Server Compiler,例如,服務器上長期運行的 Java 應用對穩定運行就有必定的要求。

在 Java7 以前,須要根據程序的特性來選擇對應的 JIT,虛擬機默認採用解釋器和其中一個編譯器配合工做。

分層編譯

Java7 引入了分層編譯,這種方式綜合了 C1 的啓動性能優點和 C2 的峯值性能優點,咱們也能夠經過參數 -client或者-server 強制指定虛擬機的即時編譯模式。

一般狀況下,C2 的執行效率比 C1 高出30%以上。

注意:在 Java8 中,默認開啓分層編譯,-client 和 -server 的設置已是無效的了。

若是隻想開啓 C2,能夠關閉分層編譯(-XX:-TieredCompilation),若是隻想用 C1,能夠在打開分層編譯的同時,使用參數:-XX:TieredStopAtLevel=1

咱們能夠經過 java -version命令行能夠直接查看到當前系統使用的編譯模式:

C:\Users\Administrator>java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

mixed mode表明是默認的混合編譯模式,除了這種模式外,咱們還可使用-Xint參數強制虛擬機運行於只有解釋器的編譯模式下,這時 JIT 徹底不介入工做;也可使用參數-Xcomp強制虛擬機運行於只有 JIT 的編譯模式下

逃逸分析

下面着重講解一下逃逸分析,這個知識點在面試的時候常常會被問到。

有這樣一個問題:咱們常說的對象,除了基本數據類型,必定是在堆上分配的嗎?

答案是否認的,經過逃逸分析,JVM 可以分析出一個新的對象的使用範圍,從而決定是否要將這個對象分配到堆上。逃逸分析如今是 JVM 的默認行爲,能夠經過參數 -XX:-DoEscapeAnalysis 關掉它。

那什麼樣的對象算是逃逸的呢?能夠看一下下面的兩種典型狀況。

如代碼所示,對象被賦值給成員變量或者靜態變量,可能被外部使用,變量就發生了逃逸。

public class EscapeAttr {
    Object attr;
    public void test() {
        attr = new Object();
    }
}

再看下面這段代碼,對象經過 return 語句返回。因爲程序並不能肯定這個對象後續會不會被使用,外部的線程可以訪問到這個結果,對象也發生了逃逸。

public class EscapeReturn {
    Object attr;
    public Object test() {
        Object obj = new Object();
        return obj;
    }
}

那逃逸分析有什麼好處呢?

1. 棧上分配

若是一個對象在子程序中被分配,指向該對象的指針永遠不會逃逸,對象有可能會被優化爲棧分配。棧分配能夠快速地在棧幀上建立和銷燬對象,不用再分配到堆空間,能夠有效地減小 GC 的壓力。

2. 分離對象或標量替換

但對象結構一般都比較複雜,如何將對象保存在棧上呢?

JIT 能夠將對象打散,所有替換爲一個個小的局部變量,這個打散的過程,就叫做標量替換(標量就是不能被進一步分割的變量,好比 int、long 等基本類型)。也就是說,標量替換後的對象,所有變成了局部變量,能夠方便地進行棧上分配,而無須改動其餘的代碼。

從上面的描述咱們能夠看到,並非全部的對象或者數組,都會在堆上分配。因爲JIT的存在,若是發現某些對象沒有逃逸出方法,那麼就有可能被優化成棧分配。

3.同步消除

若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步。

注意這是針對 synchronized 來講的,JUC 中的 Lock 並不能被消除。

要開啓同步消除,須要加上 -XX:+EliminateLocks 參數。因爲這個參數依賴逃逸分析,因此同時要打開 -XX:+DoEscapeAnalysis 選項。

好比下面這段代碼,JIT 判斷對象鎖只能被一個線程訪問,就能夠去掉這個同步的影響。

public class SyncEliminate {
    public void test() {
        synchronized (new Object()) {
        }
    }
}

小結

JIT 是現代 JVM 主要的優化點,可以顯著地提高程序的執行效率。從解釋執行到最高層次的 C2,一個數量級的性能提高也是有可能的。

注意:JIT 優化並不見得每次都有用,好比代碼中若是發生死循環。但若是你在啓動的時候,加上 -Djava.compiler=NONE 參數,禁用 JIT,它就可以執行下去。

這篇文章中咱們主要看了方法內聯、逃逸分析等概念,瞭解到一些方法在被優化後,對象並不必定是在堆上分配的,它可能在被標量替換後,直接在棧上分配。這幾個知識點也是在面試中常常被問到的。

JIT 的這些優化通常都是在後臺進程默默地去作了,咱們不須要關注太多。同時Code Cache 的容量達到上限,會影響程序執行的效率,但除非你有特別多的代碼,默認的 240M 通常來講,足夠用了。

相關文章
相關標籤/搜索