Javac編譯器詳解


學習《深入了解Java虛擬機》有一段時間了,大概理解了Java從源代碼編譯到執行出結果的過程,也能明確的知道Java是半解釋性語言。在執行源代碼時,先通過Javac編譯器對源代碼進行詞法分析、語法分析、生成抽象語法樹、語義分析等,這部分操作是在Java虛擬機之外進行的,而解釋器在虛擬機內部,所以Java程序的編譯就是半獨立的實現過程。

一、了解一下javac編譯的詳解過程

編譯過程大致上分為三步:解析與填充符號表過程、插入式注解處理器的注解處理過程、分析與字節碼生成過程。

 (1)詞法、語法分析

  詞法分析是將源代碼的字符流轉變為標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符都可以稱為標記,如“int a = b + 2”這句代碼包含了6個標記,不可拆分,分別為int、a、=、b、+、2,雖然關鍵字int由3個字符構成,但是它只是一個標記(Token),不可再拆分。
  語法分析是根據Token序列構成抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼注釋等都可以是一個語法結構。

(2)符號填充表(目前這點知識我不是很理解)

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

(3)注解處理器

  在JDK 1.5之后,Java語言提供了對注解(Annotation)的支持,這些注解與普通的Java代碼一樣,是在運行期間發揮作用的。在JDK 1.6中提供了一組插入式注解處理器的標准API在編譯期間對注解進行處理,我們可以把它看做是一組編譯器的插件,在這些插件里面可以讀取、修改、添加抽象語法樹中的任意元素。如果這些插件在處理注解期間對語法樹進行修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式注解處理器都沒有再對語法樹進行修改為止,每一次循環稱為一個Round,也就是上圖的回環過程。
  有了編譯器注解處理器的標准API后,我們的代碼才有可能干涉編譯器的行為,由於語法樹中的任意元素,甚至包括代碼注釋都可以在插件中訪問到,所以通過插入式注解處理器實現的插件功在功能上有很大的發揮空間。

(4)語義分析與字節碼生成

  語義分析之后,編譯器獲得了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但無法表示源程序是否符合邏輯。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查!
  javac分析過程分為標注檢查以及數據及控制流分析;

  a) 標注檢查 

int a = 1;
boolean b = false;
char c = 2;
 
  后續可能會出現的賦值運算:
 
int d = a + c;
int d = b + c;
char d = a + c;
 
   后續代碼中如果出現了如上3中賦值運算的話,那它們都能構成結構正確的語法樹,但是只有第1種的寫法在語義上是沒有問題的,能夠通過編譯,其余兩種在Java語言中是不合邏輯的,無法編譯(是否符合語義邏輯必須在具體的語言與具體的上下文環境之中才有意義)。

  b) 數據及控制流分析

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

  c) 語法糖

  語法糖是指在計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,但是更方便程序員的使用。Java中最常用的語法糖主要是泛型、變長參數、自動裝箱/拆箱、條件編譯等,虛擬機不支持這些語法,他們在編譯階段還原回簡單的基礎語法結構(泛型的擦除、變長參數封裝成數組參數、Integer自動裝箱拆箱變為Integer.value()等、分支不成立的代碼塊清除掉)。

(4)字節碼生成

  字節碼生成是Javac編譯過程的最后一個階段,在Javac源碼里面有com.sun.tools.javac.jvm.Gen類完成。字節碼生成階段不僅僅是把前面各個步驟所生成的信息(語法樹、符號表)轉化為字節碼寫到磁盤中,編譯器還進行了少量的代碼添加和轉換工作。  

 二、Java語法糖的味道

  語法糖雖然不會提供實質性的功能改進,但是它們或能提供效率,或能提升語法的嚴謹性,或能減少編碼出錯的機會。但是大量添加和使用含糖的語法,容易讓程序員產生依賴,無法看清程序代碼的真實面目。

(1)泛型與類型擦除

  泛型早期在Java語言中沒有出現時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型轉化。例如:在哈希表的存取中,1.5之前使用HashMap的get()方法,返回值就是一個Object對象,由於Java語言里面所有的類型都繼承於java.lang.Object,所以Object轉型成任何對象都是有可能的。但是因為有無限可能性,許多ClassCastException的風險就會轉嫁到程序的運行期間。

  泛型重載(編譯不通過)

 

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

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

 

  編譯不通過,泛型在編譯階段進行擦除,變成原生類型List<E>,擦除 動作導致這兩種方法的特征簽名變得一模一樣。

(2)自動裝箱、拆箱與遍歷循環

  源代碼:

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 (Integer i : list) {
            sum += i;
        }
        System.out.println(sum);
 }

  編譯后的代碼:

public static void main(String[] args) {
        List<Integer> 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 n = 1;

  拆箱:包裝類型變為基本類型,例如:int i = n;

  重點:包裝類重新賦值會創建新的對象,因為每一個包裝類型中value都被final修飾的!例如Integer類中:private final int value;

(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語句才能達到上述效果,編譯器將會把分支中不成立的代碼塊消除掉,這一過程在編譯階段完成!

  重點理解:在前端編譯器中,“優化”手段主要用於提升程序的編碼效率,之所以把Javac這類將Java代碼轉變為字節碼的編譯器稱做“前端編譯器”,是因為它只完成了從程序到抽象語法樹或中間代碼的生成,而在此之后,還有一組內置於虛擬機內部的“后端編譯器”完成從字節碼生成本地機器碼的過程,就是即時編譯器或JIT編譯器,這個編譯器的編譯速度及編譯結果的優劣,是衡量虛擬機性能一個很重要的指標!

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM