編譯器早期優化

1 概述前端

Java語言的「編譯期」實際上是一段「不肯定」的操做過程,由於它多是指一個前端編譯器把*.java文件轉變成*.class文件的過程;也多是指虛擬機的後端運行期編譯器(JIT編譯器,Just In Time Compiler)把字節碼轉變成機器碼的過程;還多是指使用靜態提早編譯器(AOT編譯器,Ahead Of Time Compiler)直接把*.java文件編譯成本地機器代碼的過程。java

前端編譯器:Sun的Javac、 Eclipse JDT中的增量式編譯器(ECJ)。
JIT編譯器:HotSpot VM的C一、 C2編譯器。
AOT編譯器:GNU Compiler for the Java(GCJ)、 Excelsior JET。程序員

Javac這類編譯器對代碼的運行效率幾乎沒有任何優化措施(在JDK 1.3以後,Javac的-O優化參數就再也不有意義)。 虛擬機設計團隊把對性能的優化集中到了後端的即時編譯器中,這樣可讓那些不是由Javac產生的Class文件(如JRuby、 Groovy等語言的Class文件)也一樣能享受到編譯器優化所帶來的好處。 可是Javac作了許多針對Java語言編碼過程的優化措施來改善程序員的編碼風格和提升編碼效率。 至關多新生的Java語法特性,都是靠編譯器的「語法糖」來實現,而不是依賴虛擬機的底層改進來支持,能夠說,Java中即時編譯器在運行期的優化過程對於程序運行來講更重要,而前端編譯器在編譯期的優化過程對於程序編碼來講關係更加密切。後端

2 Javac編譯器數組

從Sun Javac的代碼來看,編譯過程大體能夠分爲3個過程,分別是函數

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

 

2.1解析與填充符號表優化

2.1.1 詞法、 語法分析編碼

詞法分析是將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、 變量名、 字面量、 運算符均可以成爲標記,如「int a=b+2」這句代碼包含了6個標記,分別是int、 a、 =、 b、 +、 2,雖然關鍵字int由3個字符構成,可是它只是一個Token,不可再拆分。加密

語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract SyntaxTree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個節點都表明着程序代碼中的一個語法結構(Construct),例如包、 類型、 修飾符、 運算符、 接口、 返回值甚至代碼註釋等均可以是一個語法結構。

2.1.2. 填充符號表

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

2.2 註解處理器

咱們能夠把它看作是一組編譯器的插件,在這些插件裏面,能夠讀取、 修改、 添加抽象語法樹中的任意元素。 若是這些插件在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程從新處理,直到全部插入式註解處理器都沒有再對語法樹進行修改成止,每一次循環稱爲一個Round。

 

再次觀察上圖便可搞清楚註解處理部分。

2.3 語義分析與字節碼生成

語法分析以後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。 而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。

int a=1;
boolean b=false;
char c=2;

後續可能出現的賦值運算:

int d=a+c;
int d=b+c;
char d=a+c;

  

後續代碼中若是出現瞭如上3種賦值運算的話,那它們都能構成結構正確的語法樹,可是隻有第1種的寫法在語義上是沒有問題的,可以經過編譯,其他兩種在Java語言中是不合邏輯的,沒法編譯(是否合乎語義邏輯必須限定在具體的語言與具體的上下文環境之中才有意義。 如在C語言中,a、 b、 c的上下文定義不變,第二、 3種寫法都是能夠正確編譯)。

2.3.1 標註檢查

語義分析過程分爲標註檢查以及數據及控制流分析兩個步驟。

標註檢查步驟檢查的內容包括諸如變量使用前是否已被聲明、 變量與賦值之間的數據類型是否可以匹配等。 在標註檢查步驟中,還有一個重要的動做稱爲常量摺疊,若是咱們在代碼中寫了以下定義:

int a=1+2;

那麼在語法樹上仍然能看到字面量「1」、 「2」以及操做符「+」,可是在通過常量摺疊以後,它們將會被摺疊爲字面量「3」,因爲編譯期間進行了常量摺疊,因此在代碼裏面定義「a=1+2」比起直接定義「a=3」,並不會增長程序運行期哪怕僅僅一個CPU指令的運算量。

2.3.2數據及控制流分析

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

//方法一帶有final修飾

public void foo(final int arg){
    final int var=0;
    //do something
}

//方法二沒有final修飾

public void foo(int arg){
    int var=0;
    //do something
} 

在這兩個foo()方法中,第一種方法的參數和局部變量定義使用了final修飾符,而第二種方法則沒有,在代碼編寫時程序確定會受到final修飾符的影響,不能再改變arg和var變量的值,可是這兩段代碼編譯出來的Class文件是沒有任何一點區別的,經過第6章的講解咱們已經知道,局部變量與字段(實例變量、 類變量)是有區別的,它在常量池中沒CONSTANT_Fieldref_info的符號引用,天然就沒有訪問標誌(Access_Flags)的信息,甚至可能連名稱都不會保留下來(取決於編譯時的選項),天然在Class文件中不可能知道一個局部變量是否是聲明爲final了。 所以,將局部變量聲明爲final,對運行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障。

2.3.3解語法糖

語法糖指在計算機語言中添加的某種語法,這種語法對語言的
功能並無影響,可是更方便程序員使用。 一般來講,使用語法糖可以增長程序的可讀性,從而減小程序代碼出錯的機會。

Java中最經常使用的語法糖主要是前面提到過的泛型、 變長參數、 自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。

2.3.4字節碼生成

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

例如,前面章節中屢次提到的實例構造器<init>( )方法和類構造器<clinit>( )方法就是在這個階段添加到語法樹之中的(注意,這裏的實例構造器並非指默認構造函數,若是用戶代碼中沒有提供任何構造函數,那編譯器將會添加一個沒有參數的、 訪問性(public、 protected或private)與當前類一致的默認構造函數,<clinit>( )方法和<init>( )方法保證全部的初始化按順序進行。

3 Java語法糖的味道

3.1泛型與類型擦除

在Java語言處於尚未出現泛型的版本時,只能經過Object是全部類型的父類和類型強制轉換兩個特色的配合來實現類型泛化。 例如,在哈希表的存取中,JDK 1.5以前使用HashMap的get()方法,返回值就是一個Object對象,因爲Java語言裏面全部的類型都繼承於java.lang.Object,因此Object轉型成任何對象都是有可能的。 可是也由於有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object究竟是個什麼類型的對象。 在編譯期間,編譯器沒法檢查這個Object的強制轉型是否成功,若是僅僅依賴程序員去保障這項操做的正確性,許多ClassCastException的風險就會轉嫁到程序運行期之中。

泛型是JDK 1.5的一項新增特性,Java語言中的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,而且在相應的地方插入了強制轉型代碼,所以,對於運行期的Java語言來講,ArrayList<int>與ArrayList<String>就是同一個類,因此泛型技術其實是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。

泛型擦除前的例子:

public static void main(String[]args){
    Map<String,String>map=new HashMap<String,String>();
    map.put("hello","你好");
    map.put("how are you?","吃了沒?");
    System.out.println(map.get("hello"));
    System.out.println(map.get("how are you?"));
}

泛型擦除後的例子

public static void main(String[]args){
    Map map=new HashMap();
    map.put("hello","你好");
    map.put("how are you?","吃了沒?");
    System.out.println((String)map.get("hello"));
    System.out.println((String)map.get("how are you?"));
}

  

泛型擦除確實可能會帶來不少問題:

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)");
   }
}

  

這段代碼是不能被編譯的,由於參數List<Integer>和List<String>編譯以後都被擦除了,變成了同樣的原生類型List<E>,擦除動做致使這兩種方法的特徵簽名變得如出一轍。 初步看來,沒法重載的緣由已經找到了,但真的就是如此嗎?只能說,泛型擦除成相同的原生類型只是沒法重載的其中一部分緣由,請再接着看一看下面代碼清單的內容。

public class GenericTypes{
    public static String method(List<String>list){
        System.out.println("invoke method(List<String>list)");
        return"";
    }
    public static int method(List<Integer>list){
        System.out.println("invoke method(List<Integer>list)");
        return 1;
    }
    public static void main(String[]args){
        method(new ArrayList<String>());
        method(new ArrayList<Integer>());
    }
}

執行結果:

invoke method(List<String>list)
invoke method(List<Integer>list)

  方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名之中,因此返回值不參與重載選擇,可是在Class文件格式之中,只要描述符不是徹底一致的兩個方法就能夠共存。 也就是說,兩個方法若是有相同的名稱和特徵簽名,但返回值不一樣,那它們也是能夠合法地共存於一個Class文件中的。

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

public static void main(String[]args){
    List<Integer>list=Arrays.asList(1,2,3,4);
    //若是在JDK 1.7中,還有另一顆語法糖[1]
    //能讓上面這句代碼進一步簡寫成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[]args){
    List list=Arrays.asList(new Integer[]{
    Integer.valueOf(1),
    Integer.valueOf(2),
    Integer.valueOf(3),
    Integer.valueOf(4)});
    int sum=0;
    for(Iterator localIterator=list.iterator();localIterator.hasNext();){
    int i=((Integer)localIterator.next()).intValue();
    sum+=i;
    }
   System.out.println(sum);
} 

自動裝箱、 拆箱在編譯以後被轉化成了對應的包裝和還原方法,如本例中的Integer.valueOf()與Integer.intValue()方法,而遍歷循環則把代碼還原成了迭代器的實現,這也是爲什麼遍歷循環須要被遍歷的類實現Iterable接口的緣由。 最後再看看變長參數,它在調用的時候變成了一個數組類型的參數,在變長參數出現以前,程序員就是使用數組來完成相似功能的。

這些語法糖雖然看起來很簡單,但也不見得就沒有任何值得咱們注意的地方 ,好比包裝類的「==」運算在不遇到算術運算的狀況下不會自動拆箱,以及它們equals()方法不處理數據轉型的關係

 

public static void main(String[]margs){
		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);
		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));
} 
true
false
true
true
true
false

  

3.3 條件編譯

public static void main(String[]args){
    if(true){
    System.out.println("block 1");
    }else{
    System.out.println("block 2");
    }
}

反編譯後

public static void main(String[]args){
    System.out.println("block 1");
}

 

只能使用條件爲常量的if語句才能達到上述效果,若是使用常量與其餘帶有條件判斷能力的語句搭配,則可能在控制流分析中提示錯誤,被拒絕編譯

 

public static void main(String[]args){
    //編譯器將會提示「Unreachable code」
    while(false){
    System.out.println("");
   }
}
相關文章
相關標籤/搜索