JVM(1)---虛擬機在運行期的優化策略

1.解釋器與JIT編譯器

首先咱們先來了解一下運行在虛擬機之上的解釋器JIT編譯器java

當咱們的虛擬機在運行一個java程序的時候,它能夠採用兩種方式來運行這個java程序:c++

  1. 採用解釋器的形式,也就是說,在運行.class運行的時候,解釋器一邊把.class文件翻譯成本地機器碼,一邊執行。顯然這種一邊解釋翻譯一邊執行發方式,可使咱們當即啓動和執行程序,省去編譯的時間。不過因爲須要一遍解釋翻譯,會讓程序的執行速度比較慢。
  2. 採用JIT編譯器的方式:注意,JIT編譯器是把.class文件翻譯成本地機器碼,而javac編譯器是把.java源文件編譯成.class文件。若是採用JIT編譯器的方式則是在啓動運行一個程序的時候,先把.class文件所有翻譯成本地機器碼,而後再來執行,顯然,這種方式在執行的時候因爲不用對.clasa文件進行翻譯,因此執行的速度會比較快。固然,代價就是咱們須要花銷必定的時間來把字節碼翻譯成本地機器碼。這樣,程序在啓動的時候,會有更多的延遲。

這兩種方式能夠說是各有優點,虛擬機(特指HotSpot虛擬機)在執行的時候,通常會採用兩種方式結合的策略。算法

也就是說,在程序執行的時候,有些代碼採用解釋器的方式,有些代碼採用編譯器,稱之爲即時編譯。通常咱們會對熱點代碼採用編譯器的方式。編程

2.編譯對象與觸發條件

上面已經說了,運行過程當中,若是遇到熱點代碼就會觸發對該代碼進行編譯,編譯成本地機器碼。數組

什麼是熱點代碼?安全

熱點代碼主要有一下兩類:微信

  1. 被屢次調用的方法。
  2. 被屢次執行的循環體。

不過這裏須要注意的是,因爲循環體是存在方法之中的,儘管編譯動做是由循環體觸發的,但編譯器仍然會以這個方法來做爲編譯的對象。性能

3.熱點探測

判斷一段代碼是否是熱點代碼,是否是須要觸發即時編譯,這樣的行爲咱們稱之爲熱點探測。熱點探測斷定有如下兩種方式:優化

  1. 基於採樣的熱點探測:這種方式虛擬機會週期性着檢查各個線程的棧頂,若是發現某個方法常常出如今棧頂,那麼這個方法就是熱點方法。可能有人會問,所謂常常,那什麼樣纔算常常,對於這個我只能告訴你,這個取決於你本身的設置,若是本身沒有進行相應的設置的話,就採用虛擬機的默認設置。
  2. 基於計數器的熱點探測:這種方法咱們會爲每一個方法設置一個計數器,統計方法被調用的次數,若是到達必定的次數,咱們就把它看成是熱點方法

兩種方法的優缺點線程

顯然第一種方法在實現上是比較簡單、高效的,可是缺點也很明顯,精確度不高,容易受到線程阻塞等別的外界因素的干擾。

第二種方式的統計結果會很精確,但須要爲每一個方法創建並維護一個計數器。實現上會相對複雜一點而且開銷也會大點。

不過,這裏須要指出的是,咱們的HotSpot虛擬機採用的是基於計數器的方式。

說明:虛擬機在執行方法的時候,會先判斷該方法是否存在已經編譯好的版本,若是存在,則執行編譯好的 本地機器碼,不然,採用一邊解釋一邊編譯的方式。

4.編譯優化技術

先看一段代碼:

int a = 1;
if(false){
    System.out.println("無用代碼");
}
int b = 2;

對於這段代碼,咱們都知道是if語句體裏面的代碼是必定不可能會被執行到的,也就是說,這其實是一段一點用處也沒有的代碼,在執行時只能浪費判斷時間。

實際上,對於咱們書寫的代碼,編譯器在編譯的時候是會進行優化的。對於上面的代碼,編譯優化以後會變成這樣:

int a = 1;
int b = 2;

那段無用的代碼會被消除掉。

各類編譯優化策略

咱們剛纔已經說了,對於有些被屢次調用的方法或者循環體,虛擬機會先把他們編譯成本地機器碼。因爲這些熱點代碼都是一些會被屢次重複執行的代碼,爲了使得編譯好的代碼更加完美,運行的更快。編譯器作了不少的編譯優化策略,例如上面的無用代碼消除就是其中的一種。

下面咱們來說講大概都有那些優化策略:

大概預覽一波:

  1. 公共子表達式消除。
  2. 數組範圍檢查消除。
  3. 方法內聯。
  4. 逃逸分析。

(1).公共子表達式消除

含義:若是一個表達式 E 已經計算過了,而且從先前的計算到如今 E 中的全部變量的值都沒有發生變化,那個 E 的此次出現就成爲了公共子表達式。對於這樣的表示式,沒有必要對它再次進行計算了,直接沿用以前的結果就能夠了。

咱們來舉個例子。例如

int d = (c * b) * 10 + a + (a + b * c);

這段代碼到了即時編譯器的手裏,它會進行以下優化:

表達式中有兩個 b * c的表達式,而且在計算期間b與c的值並不會變。因此這條表達式可能會被視爲:

int d = E * 10 + a+ (a + E);

接着繼續優化成

int d = E * 11 + a + a;

接着

int d = E * 11 + 2a;

這樣,代碼在執行的時候,就會節省了一些時間了。

(2).數組範圍檢查消除

咱們知道,java是一門動態安全的語言,對數組的訪問不像c/c++那樣,能夠採用指針指向一塊可能不存在的區域。例如假若有一個數組arr[],在java語言中訪問數組arr[i]的時候,是會先進行上下界範圍檢查的,即先檢查i是否知足i >= 0 && i < arr.length這個條件。若是不知足則會拋出相應的異常。這種安全檢查策略能夠避免溢出。但每次數組訪問都會進行這樣一次檢查無疑在速度性能上形成必定的影響。

實際上,對於這樣一種狀況,編譯器也是能夠幫助咱們作出相應的優化的。例如對於數組的下標是一個常量的,如arr[2],只要在編譯期根據數據流分析來肯定arr.length的值,並判斷下標‘2’並無越界,這樣在執行的時候就無需在判斷了。

更常見的狀況是數組訪問發生在循環體中,而且使用循環變量來進行數組的訪問,對於這樣的狀況,只要編譯器經過數據流就能夠判斷循環變量的取值範圍是否在[0, arr.length)以內,若是是,那麼整個循環中就能夠節省不少次數組邊界檢測判斷的操勞了。

對於這些安全檢查所消耗的時間,實際上,咱們還能夠採用另一種策略--隱式異常處理。例如當咱們在訪問一個對象arr的屬性arr.value的時候,沒有優化以前虛擬機是這樣處理的:

if(arr != null){
    return arr.value;
}else{
    throw new NollPointException();
}

採用優化策略以後編程這樣子:

try{
    return arr.value;
}catch(segment_fault){
    uncommon_trap();
}

就是說,虛擬機會註冊一個Segment Fault信號的異常處理器(uncommon_trap()),這樣當arr不爲空的時候,對value的訪問能夠省去對arr的判斷。代價就是當arr爲空時,必須轉入到異常處理器中恢復並拋出NullPointException異常,這個過程會從用戶態轉到內核態中處理,結束後在回到用戶態,速度遠比一次判斷空檢查慢。當arr極少爲null的時候,這樣作是值得的,但假如arr常常爲null時,那麼會得不償失。

不過,虛擬機仍是挺聰明的,它會根據運行期收集到的信息來自動選擇最優方案。

(3).方法內聯

先看一段代碼

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}

public static void test(String[] args){
    Object obj = null;
    f(obj);
}

對於這段代碼,若是把兩個方法結合在一塊兒看,咱們能夠發現test()方法裏面都是一些無用的代碼。由於f(obj)這個方法的調用,沒啥卵用。可是若是不作內聯優化,後續儘管進行了無用代碼的消除,也是沒法發現任何無用代碼的,由於若是把f(Object obj)和test(String[] args)兩個發放分開看的話,咱們就沒法得只f(obj)是否有用了。

內聯優化後的代碼能夠是這樣:

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}

public static void test(String[] args){
    Object obj = null;
    //該方法直接不執行了
}

(4).逃逸分析

逃逸分析是目前Java虛擬機比較前沿的優化技術,它並不是是直接優化代碼,而是爲其餘優化手段提供依據發分析技術。

逃逸分析主要是對對象動態做用域進行分析:當一個對象在某個方法被定義後,它有可能被外部的其餘方法所引用,例如做爲參數傳遞給其餘方法,稱之爲方法逃逸,也有可能被外部線程訪問到,例如類變量,稱之爲線程逃逸

假如咱們能夠證實一個對象並不會發生逃逸的話,咱們就能夠經過一些方式對這個變量進行一些高效的優化了。以下所示:

1).棧上分配

咱們都知道一個對象建立以後是放在上的,這個對象能夠被其餘線程所共享,而且咱們知道在堆上的對象若是再也不使用時,虛擬機的垃圾收集系統就會對它進行帥選並回收。但不管是回收仍是帥選,都是須要花費時間的。

可是假如咱們知道這個對象不會逃逸的話,咱們就能夠直接在棧上對這個對象進行內存分配了,這樣,這個對象所佔用的內存空間就能夠隨進棧和出棧而自動被銷燬了。這樣,垃圾收集系統就能夠省了不少帥選、銷燬的時間了。

2).同步消除

線程同步自己是一個相對耗時的過程,若是咱們能判斷這個變量不會逃出線程的話,那麼咱們就能夠對這個變量的同步措施進行消除了。

3).標量替換

什麼是標量?

當一個數據沒法分解成更小的時候,咱們稱之爲變量,例如像int,long,char等基本數據類型。相對地,若是一個變量能夠分解成更小的,咱們稱之爲聚合量,例如Java中的對象。

假如這個對象不會發生逃逸。

咱們能夠根據程序訪問的狀況,若是一個方法只是用到一個對象裏面的若干個屬性,咱們在真正執行這個方法的時候,咱們能夠不建立這個對象,而是直接建立它那幾個被使用到的變量來代替。這樣,不只能夠節省內存以及時間,並且這些變量能夠隨出棧入棧而銷燬。

不過,對於編譯器優化的技術還有不少,上面這幾種算是比較典型的。

本次講解到這裏。

參考書籍:深刻Java虛擬機

若是你習慣在微信公衆號看技術文章
想要獲取更多資源的同窗
歡迎關注個人公衆號: 苦逼的碼農 每週不定時更新文章,同時更新本身算法刷題記錄。
相關文章
相關標籤/搜索