早期(編譯期)優化

早期(編譯期)優化



Basic

  • Java語言的“編譯期”, 前端編譯器(其實叫“編譯器的前端”更準確一些)把xx.java文件轉變成xx.class文件的過程(普遍認知);也可能是指虛擬機的後端運行期編譯器(JIT編譯器,Just In Time Compiler)把字節碼轉變成機器碼的過程;還可能是指使用靜態提前編譯器(AOT編譯器,Ahead Of Time Compiler)直接把xx.java文件編譯成本地機器代碼的過程。
  • 虛擬機設計團隊把對性能的優化集中到了後端的即時編譯器中,這樣可以讓那些不是由Javac產生的Class文件(如JRuby、 Groovy等語言的Class文件)也同樣能享受到編譯器優化所帶來的好處。相當多新生的Java語法特性,都是靠編譯器的“語法糖”來實現,而不是依賴虛擬機的底層改進來支持。

Javac編譯器

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

ch10-javac-compiler-process.png-92.9kB

ch10-javac-compile-process-2.png-155.1kB

解析與填充符號表

詞法、 語法分析

  • 詞法分析是將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、 變量名、 字面量、 運算符都可以成爲標記,如“int a=b+2”這句代碼包含了6個標記,分別是int、a、=、b、+、2,雖然關鍵字int由3個字符構成,但是它只是一個Token,不可再拆分。在Javac的源碼中,詞法分析過程由com.sun.tools.javac.parser.Scanner類來實現。
  • 語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等都可以是一個語法結構。構造了AST之後,編譯器就基本不會再對源碼文件進行操作了,後續的操作都建立在抽象語法樹之上。

填充符號表

  • 符號表(Symbol Table)是由一組符號地址和符號信息構成的表格,讀者可以把它想象成哈希表中K-V值對的形式(實際上符號表不一定是哈希表實現,可以是有序符號表、樹狀符號表、棧結構符號表等)。 符號表中所登記的信息在編譯的不同階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

註解處理器

  • 註解(Annotation)在運行期間發揮作用;

語義分析與字節碼生成

  • 語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查(類型是否匹配,支持某種操作,否則無法通過編譯)。

標註檢查

  • Javac的編譯過程中,語義分析過程分爲標註檢查, 數據及控制流分析
    • 標註檢查:檢查變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等;還有常量摺疊 – a = 1 + 2, AST樹上還能看到各個token,經過常量摺疊後,它們會摺疊爲常量3。
    • 標註檢查步驟在Javac源碼中的實現類是com.sun.tools.javac.comp.Attr類和com.sun.tools.javac.comp.Check類

數據及控制流分析

  • 數據及控制流分析是對程序上下文邏輯更進一步的驗證,它可以檢查出諸如程序局部變量在使用前是否有賦值、 方法的每條路徑是否都有返回值、 是否所有的受查異常都被正確處理了等問題。
  • 編譯時期的數據及控制流分析與類加載時的數據及控制流分析的目的基本上是一致的,但校驗範圍有所區別,有一些校驗項只有在編譯期或運行期才能進行。
  • Class文件中不可能知道一個局部變量是不是聲明爲final;局部變量與字段(實例變量、類變量)是有區別的,它在常量池中沒有CONSTANT_Fieldref_info的符號引用,自然就沒有訪問標誌(Access_Flags)的信息,甚至可能連名稱都不會保留下來。將局部變量聲明爲final,對運行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障
  • Javac的源碼中,數據及控制流分析的入口是圖10-5中的flow()方法,具體操作由com.sun.tools.javac.comp.Flow類來完成。

解語法糖

  • 語法糖(Syntactic Sugar),指在計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用。Java中最常用的語法糖主要是前面提到過的泛型、變長參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。
  • Javac的源碼中,解語法糖的過程由desugar()方法觸發,在com.sun.tools.javac.comp.TransTypes類和com.sun.tools.javac.comp.Lower類中完成

字節碼生成

  • 字節碼生成階段不僅僅是把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,編譯器還進行了少量的代碼添加和轉換工作。
  • 實例構造器<init>()方法和類構造器<clinit>()方法就是在這個階段添加到語法樹之中的(注意,這裏的實例構造器並不是指默認構造函數,如果用戶代碼中沒有提供任何構造函數,那編譯器將會添加一個沒有參數的、 訪問性(public、protected或private)與當前類一致的默認構造函數 – 在填充符號表階段就已經完成),這兩個構造器的產生過程實際上是一個代碼收斂的過程,編譯器會把語句塊(對於實例構造器而言是“{}”塊,對於類構造器而言是“static{}”塊)、變量初始化(實例變量和類變量)、調用父類的實例構造器(僅僅是實例構造器,<clinit>()方法中無須調用父類的<clinit>()方法,虛擬機會自動保證父類構造器的執行,但在<clinit>()方法中經常會生成調用java.lang.Object的<init>()方法的代碼)等操作收斂到<init>()和<clinit>()方法之中,並且保證一定是按先執行父類的實例構造器,然後初始化變量,最後執行語句塊的順序進行,上面所述的動作由Gen.normalizeDefs()方法來實現。除了生成構造器以外,還有其他的一些代碼替換工作用於優化程序的實現邏輯,如把字符串的加操作替換爲StringBuffer或StringBuilder(取決於目標代碼的版本是否大於或等於JDK 1.5)的append()操作等。
  • 完成了對語法樹的遍歷和調整之後,就會把填充了所有所需信息的符號表交給com.sun.tools.javac.jvm.ClassWriter類,由這個類的writeClass()方法輸出字節碼,生成最終的Class文件,到此爲止整個編譯過程宣告結束。
    • Javac源碼裏面由com.sun.tools.javac.jvm.Gen類來完成。

Java語法糖的味道

泛型與類型擦除

  • 本質是參數化類型(Parametersized Type),也就是說所操作的數據類型被指定爲一個參數,JDK1.5之前,如果僅僅依賴程序員去類型轉換的正確性,許多ClassCastException的風險就會在程序運行期出現。
  • C#裏面泛型無論在程序源碼中、 編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符),或是運行期的CLR中,都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱爲類型膨脹,基於這種方法實現的泛型稱爲真實泛型
  • Java語言中的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,並且在相應的地方插入了強制轉型代碼,因此,對於運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型
  • Java泛型寫就的代碼,編譯後進行反編譯,發現泛型不見了。泛型變成了原生類型。
public class GenericTypes {

    public static void method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
    }

    public static void method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
    }
}
# compile
Error:(15, 24) java: 名稱衝突:method(java.util.List<java.lang.Integer>)method(java.util.List<java.lang.String>)具有相同簽名。
  • 參數List<Integer>和List<String>編譯之後都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特徵簽名變得一模一樣。 (注:書中將兩個方法的返回類型改成不一樣的,運行時就能夠編譯運行,但是實際上JDK1.8中已經不可以了,應該是嚴格遵守了方法重載要求方法具備不同的特徵簽名,返回值並不包含在方法的特徵簽名之中,所以返回值不參與重載選擇)
  • 虛擬機規範由於Java泛型引入了諸如Signature、 LocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特徵簽名,這個屬性中保存的參數類型並不是原生類型,而是包括了參數化類型的信息
  • 從Signature屬性的出現我們還可以得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們能通過反射手段取得參數化類型的根本依據。

自動裝箱、 拆箱與遍歷循環

  • 遍歷循環則把代碼還原成了迭代器的實現,這也是爲何遍歷循環需要被遍歷的類實現Iterable接口的原因
  • 自動裝箱、 拆箱在編譯之後被轉化成了對應的包裝和還原方法,如本例中的Integer.valueOf()與
    Integer.intValue()方法
  • 變長參數,它在調用的時候變成了一個數組類型的參數
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    // 如果在JDK 1.7中,還有另外一顆語法糖 ,
    // 能讓上面這句代碼進一步簡寫成List<Integer> list = [1, 2, 3, 4];
    int sum = 0;
    for (int i : list) {
        sum += i;
    }
    System.out.println(sum);
}
# 編譯後反編譯的代碼。
public static void main(String[] var0) {
        List var1 = Arrays.asList(new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)});
        int var2 = 0;

        int var4;
        for(Iterator var3 = var1.iterator(); var3.hasNext(); var2 += var4) {
            var4 = ((Integer)var3.next()).intValue();
        }

        System.out.println(var2);
    }
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;
    System.out.println(c == d); // true
    System.out.println(e == f); // false
    System.out.println(c == (a + b)); // true
    System.out.println(c.equals(a + b)); // true
    System.out.println(g == (a + b)); // true
    System.out.println(g.equals(a + b)); // false
}

output:
true
false
true
true
true
false
  • equals()方法不處理數據轉型的關係; Integer和Long都對-128到127進行了緩存。

條件編譯

  • Java語言當然也可以進行條件編譯,方法就是使用條件爲常量的if語句。
public static void main(String[] args) {
        if (true) {
            System.out.println("block 1");
        } else {
            System.out.println("block 2");
        }
}

# 編譯後反編譯的結果如下:
public static void main(String[] var0) {
        System.out.println("block 1");
}

ref

深入理解Java虛擬機(第二版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章