JVM性能優化系列-(5) 早期編譯優化

JVM.jpg

5. 早期編譯優化

早起編譯優化主要指編譯期進行的優化。前端

java的編譯期可能指的如下三種:java

  1. 前端編譯器:將*.java文件變成*.class文件,例如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
  2. JIT編譯器(Just In Time Compiler):將字節碼變成機器碼,例如HotSpot VM的C一、C2編譯器
  3. AOT編譯器(Ahead Of Time Compiler):直接把*.java文件編譯成本地機器碼,例如GNU Compiler for the Java(GCJ)、Excelsior JET

本文中涉及到的編譯器都僅限於第一類,第二類編譯器跟java語言的關係不大。javac這類編譯器對代碼的運行效率幾乎沒有任何優化措施,但javac作了許多針對java語言代碼過程的優化措施來改善程序員的編碼風格和提升編碼效率,java許多的語法特性都是靠編譯器的語法糖來實現的。git

5.1 javac編譯器工做流程

Sun javac編譯器的編譯過程能夠分爲3個過程:程序員

  • 解析與填充符號表過程
  • 插入式註解處理器的註解處理過程
  • 分析與字節碼生成過程

1. 解析與填充符號表

解析步驟包括了經典程序編譯原理中的詞法分析與語法分析兩個過程。github

詞法、語法分析:詞法分析是將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符均可以成爲標記 語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個節點都表明着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等均可以是一個語法結構。面試

填充符號表:符號表(Symbol Table)是由一組符號地址和符號信息構成的表格,能夠想象成K-V的形式。符號表中所登記的信息在編譯的不一樣階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據編程

2. 註解處理器

註解處理器是用於提供對註解的支持,能夠將其當作一組編譯器的插件。後端

3. 語義分析與字節碼生成

語法分析後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。數組

這部分主要分以下幾步,完成語義分析與字節碼生成:緩存

  1. 標註檢查

標註檢查檢查的內容包括變量使用前是否已被聲明、變量與賦值之間的數據類型是否可以匹配等。在標註檢查中,還有一個重要的動做稱爲常量摺疊,這使得a=1+2比起a=3不會增長任何運算量

  1. 數據及控制流分析

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

  1. 解語法糖

語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中添加的某種語法,這種語法對語言的功能並無影響,但方便使用。java在現代編程語言中屬於低糖語言,java中的主要語法糖包括泛型、可變參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖

  1. 字節碼生成

字節碼生成階段不只僅時把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,編譯器還進行了少許的代碼添加和轉換工做

5.2 Java語法糖

語法糖主要是爲了方便程序員的代碼開發,這些語法糖並不會提供實質性的功能改進,可是他們能提升效率。

如下介紹了Java中經常使用的語法糖。

泛型與類型擦除

Java中的參數化類型只在源碼中存在,在編譯後的字節碼中,已經被替換爲原來的原生類型了,而且在相應的地方插入了強制轉換代碼。對於運行期的Java 語言來講,ArrayList和ArrayList就是同一個類。因此說泛型技術實際上就是 Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。

如下兩個方法,在編譯時,因爲類型擦除,變成了同樣的原生類型List,所以方法的特徵簽名變得一致,致使沒法編譯。

void method(List<Integer> list);
void method (List<String> list);
複製代碼

可是若是二者的返回值不一致,在JDK1.6中則能夠編譯經過,並非由於返回值不一樣,因此重載成功。只是由於加入返回值後,兩個方法的字節碼特徵簽名不同了,因此能夠共存。可是在JDK1.7和1.8中,依然沒法經過,會報兩個方法在類型擦除後具備相同的特徵簽名。

Java代碼中的方法特徵簽名只包含方法名稱、參數順序和參數類型,而在字節碼中的特徵簽名還包括方法返回值及受查異常表。 方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名中,因此返回值不參與重載選擇。可是在Class字節碼文件中,只要描述符不是徹底一致的兩個方法就能夠共存。

自動裝箱和拆箱

自動裝箱和拆箱實現了基本數據類型與對象數據類型之間的隱式轉換。

public void autobox() {
    Integer one = 1;
    if (one == 1) {
        System.out.println(one);
    }
}
複製代碼

下面對自動裝箱和自動拆箱進行詳細介紹:

自動裝箱就是Java自動將原始類型值轉換成對應的對象,好比將int的變量轉換成Integer對象,這個過程叫作裝箱,反之將Integer對象轉換成int類型值,這個過程叫作拆箱。由於這裏的裝箱和拆箱是自動進行的非人爲轉換,因此就稱做爲自動裝箱和拆箱。原始類型byte,short,char,int,long,float,double和boolean對應的封裝類爲Byte,Short,Character,Integer,Long,Float,Double,Boolean。

什麼時候發生自動裝箱和拆箱,

  1. 賦值:
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
複製代碼
  1. 方法調用:當咱們在方法調用時,咱們能夠傳入原始數據值或者對象,一樣編譯器會幫咱們進行轉換。
public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer
複製代碼

自動裝箱的弊端,

自動裝箱有一個問題,那就是在一個循環中進行自動裝箱操做的時候,以下面的例子就會建立多餘的對象,影響程序的性能。

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}
複製代碼

自動裝箱與比較:

下面程序的輸出結果是什麼?

public class Main {
    public static void main(String[] args) {
         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}
複製代碼

在解釋具體的結果時,首先必須明白以下兩點:

  • 當"=="運算符的兩個操做數都是 包裝器類型的引用,則是比較指向的是不是同一個對象,而若是其中有一個操做數是表達式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。
  • 對於包裝器類型,equals方法並不會進行類型轉換。

下面是程序的具體輸出結果:

true
false
true
true
true
false
true
複製代碼

注意到對於Integer和Long,Java中,會對-128到127的對象進行緩存,當建立新的對象時,若是符合這個這個範圍,而且已有存在的相同值的對象,則返回這個對象,不然建立新的Integer對象。

對於上面的結果:

c==d:指向相同的緩存對象,因此返回true; e==f:不存在緩存,是不一樣的對象,因此返回false; c==(a+b):直接比較的數值,所以爲true; c.equals(a+b):比較的對象,因爲存在緩存,因此兩個對象同樣,返回true; g==(a+b):直接比較的數值,所以爲true; g.equals(a+b):比較對象,因爲equals也不會進行類型轉換,a+b爲Integer,g爲Long,所以爲false; g.equals(a+h):和上面不同,a+h時,a會進行類型轉換,轉成Long,接着比較兩個對象,因爲Long存在緩存,因此兩個對象一致,返回true。

關於equals和==:

  • .equals(...) will only compare what it is written to compare, no more, no less.
  • If a class does not override the equals method, then it defaults to the equals(Object o) method of the closest parent class that has overridden this method.
  • If no parent classes have provided an override, then it defaults to the method from the ultimate parent class, Object, and so you're left with the Object#equals(Object o) method. Per the Object API this is the same as ==; that is, it returns true if and only if both variables refer to the same object, if their references are one and the same. Thus you will be testing for object equality and not functional equality.
  • Always remember to override hashCode if you override equals so as not to "break the contract". As per the API, the result returned from the hashCode() method for two objects must be the same if their equals methods show that they are equivalent. The converse is not necessarily true.

遍歷循環

遍歷循環語句是java5的新特徵之一,在遍歷數組、集合方面,爲開發人員提供了極大的方便。

public void circle() {
    Integer[] array = { 1, 2, 3, 4, 5 };

    for (Integer i : array) {

    System.out.println(i);

    }
}

複製代碼

在編譯後的版本中,代碼還原成了迭代器的實現,這也是爲遍歷循環須要被遍歷的類實現Iterable接口的緣由。

變長參數

Arrays.asList(1, 2, 3, 4, 5);

複製代碼

條件編譯

條件編譯也是java語言的一種語法糖,根據布爾常量值的真假,編譯器將會把分支中不成立的代碼塊消除掉。

public void ifdef() {if (true) {

System.out.println("true");

} else {//此處有警告--DeadCode

System.out.println("false");

}

}
複製代碼

對枚舉和字符串的switch支持

public void enumStringSwitch() {

String str = "fans";

switch (str) {

case "fans":

break;case "leiwen":

break;default:

break;

}

}
複製代碼

try-with-resources

在try語句中定義和關閉資源 jdk7提供了try-with-resources,能夠自動關閉相關的資源(只要該資源實現了AutoCloseable接口,jdk7爲絕大部分資源對象都實現了這個接口)。

staticStringreadFirstLineFromFile(Stringpath)throwsIOException{

try(BufferedReaderbr=newBufferedReader(newFileReader(path))){

returnbr.readLine();
}
}
複製代碼

本文由『後端精進之路』原創,首發於博客 teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,馬上獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png
相關文章
相關標籤/搜索