深入分析 Javac 編譯原理

PatrickLee666發表於2018-09-17

Javac是什麼

通常,一個java檔案會通過編譯器編譯成位元組碼檔案.class,再又java虛擬機器JVM翻譯成計算機可執行的檔案。

我們所知道的java語言有它自己的語法規範,同樣的JVM也有它的語法規範,如何讓java的語法規則去適應語法解析規則,這就是javac的作用,簡而言之,javac的作用就是將java原始碼轉化成class位元組碼檔案。

Javac編譯器的基本結構

編譯步驟

image

1. 詞法分析器:

1.1作用:

將原始碼轉化為Token流

1.2流程

讀取原始碼,從原始檔的一個字元開始,按照java語法規範依次找出package,import,類定義,屬性,方法定義等,最後構建出一個抽象語法樹

1.3舉例

package compile;

/**
 * 詞法解析器
 */
 public class Cifa{
     int a;
     int c = a + 1;
 }
複製程式碼

轉化為Token流:

image

1.4原始碼分析

  • com.sun.tools.javac.parser.JavacParser  規定哪些詞符合Java語言規範,具體讀取和歸類不同詞法的操作由scanner完成
  • com.sun.tools.javac.parser.Scanner  負責逐個讀取原始碼的單個字元,然後解析符合Java語言規範的Token序列,呼叫一次nextToken()都構造一個Token
  • com.sun.tools.javac.parser.Tokens$TokenKind  裡面包含了所有token的型別,譬如BOOLEAN,BREAK,BYTE,CASE。
  • com.sun.tools.javac.util.Names  用來儲存和表示解析後的詞法,每個字符集合都會是一個Name物件,所有的物件都儲存在Name.Table這個內部類中。
  • com.sun.tools.javac.parser.KeyWords  負責將字符集合對應到token集合中,如,package zxy.demo.com; Token.PACKAGE = package, Token.IDENTIFIER = zxy.demo.com,(這部分又分為讀取第一個token,為zxy,判斷下一個token是否為“.”,是的話接著讀取下一個Token.IDENTIFIER型別的token,反覆直至下一個token不是”.”,也就是說下一個不是Token.IDENIFIER型別的token,Token.SEMI = ;即這個TIDENTIFIER型別的token的Name讀完),KeyWords類負責此任務。

1.5問題

Javac是如何分辨這一個個Token呢?例如它時如何直到package是關鍵詞而不是自定義變數呢?

Javac在進行此法分析時會由JavacParser根據Java語言規範來控制什麼順序,地方會出現什麼Token,例如package就只能在檔案的最開頭出現

Javac怎樣確定哪些字元組合在一起就是一個Token呢?它如何從一串字元流中劃分出Token來?

對於關鍵字,主要由關鍵字的語法規則,例如package就是若一個字串package是連續的,那麼他就是關鍵字

對於自定義變數名稱,自定義名稱之間用空格隔開,每個語法表示式用分號結束

舉例:

int a = 1 + 2;

從package開始

.....

int 就是通過語法關鍵字判定的TOKEN:INT

int a之間通過空格隔開

a 就是自定義的變數被判定為TOKEN:IDENTIFIER

a =之間通過空格隔開(這時有的小夥伴就會說了,int a=b+c;這句話也不報錯啊,對的,大多數時候,這種不用空格分開確實能夠編譯,這是因為java指出宣告變數的時候必須以字母、下劃線或者美元符開頭,當JavacParser讀完a去讀=的時候就直到這個=不屬於變數了)將=判定為TOKEN:EQ

1被判定為TOKEN:INTLITERAL

.....

將;識別為TOKEN:SEMI

.....

最後讀取到類結束,也就是}被判定為TOKEN:RBRACE

2.語法分析器:

剛才,詞法解析器已經將Java原始檔解析成了Token流。

現在,語法解析器就要將Token流組建成更加結構化的語法樹。也就是將這些Token流中的單詞裝成一句話,完整的語句。

2.1作用

將進行詞法分析後形成的Token流中的一個個Token組成一句句話,檢查這一句句話是不是符合Java語言規範。

2.2語法分析三部分

  • package
  • import
  • 類(包含class、interface、enum),一下提到的類泛指這三類,並不單單是指class

2.3所用類庫

  • com.sun.tools.javac.tree.TreeMaker  所有語法節點都是由它生成的,根據Name物件構建一個語法節點
  • com.sun.tools.javac.tree.JCTree$JCIf   所有的節點都會繼承jctree和實現**tree,譬如 JCIf extends JCTree.JCStatement implements IfTree
  • com.sun.tools.javac.tree.JCTree的三個屬性
    • Tree tag:每個語法節點都會以整數的形式表示,下一個節點在上一個節點上加1;
      複製程式碼
    • pos:也是一個整數,它儲存的是這個語法節點在原始碼中的起始位置,一個檔案的位置是0,而-1表示不存在
      複製程式碼
    • type:它代表的是這個節點是什麼java型別,如int,float,還是string等
      複製程式碼

2.4 舉例

package compile;

/**
 * 語法
 */
public class Yufa {
    int a;
    private int c = a + 1;
    
    //getter
    public int getC() {
        return c;
    }
    //setter
    public void setC(int c) {
        this.c = c;
    }
}
複製程式碼

image

  • 每一個包package下的所有類都會放在一個JCCompilationUnit節點下,在該節點下包含:package語法樹(作為pid)、各個類的語法樹
  • 每一個從JCClassDecl發出的分支都是一個完整的程式碼塊,上述是四個分支,對應我們程式碼中的兩行屬性操作語句和兩個方法塊程式碼塊,這樣其實就完成了語法分析器的作用:將一個個Token單片語成了一句句話(或者說成一句句程式碼塊)
  • 在上述的語法樹部分,對於屬性操作部分是完整的,但是對於兩個方法塊,省略了一些語法節點,例如:方法修飾符public、方法返回型別、方法引數。

注1:若類中有import關鍵字則途中還有import的語法節點

注2:所有語法節點的生成都是在TreeMaker類中完成的

3.語義分析器

3.1作用

將語法樹轉化為註解語法樹,即在這顆語法樹上做一些處理

3.2步驟

  • 給類新增預設建構函式(由com.sun.tools.javac.comp.Enter類完成)

  • 處理註解(由com.sun.tools.javac.processing.JavacProcessingEnvironment類完成)

  • 檢查語義的合法性並進行邏輯判斷(由com.sun.tools.javac.comp.Attr完成)

    • 變數的型別是否匹配
    • 變數在使用前是否初始化
    • 能夠推匯出泛型方法的引數型別
    • 字串常量合併
  • 資料流分析(由com.sun.tools.javac.comp.Flow類完成)

    • 檢驗變數是否被正確賦值(eg.有返回值的方法必須確定有返回值)
    • 保證final變數不會被重複修飾
    • 確定方法的返回值型別
    • 所有的檢查型異常是否丟擲或捕獲
    • 所有的語句都要被執行到(return後邊的語句就不會被執行到,除了finally塊兒)
  • 對語法樹進行語義分析(由com.sun.tools.javac.comp.Flow執行)

    • 去掉無用的程式碼,如只有永假的if程式碼塊
    • 變數的自動轉換,如將int自動包裝為Integer型別
    • 去除語法糖,將foreach的形式轉化為更簡單的for迴圈

最終,生成了註解語法樹

3.3所用類庫

  • com.sun.tools.javac.comp.Check,它用來輔助Attr類檢查語法樹中變數型別是否正確,如方法返回值是否和接收的引用值型別匹配
  • com.sun.tools.javac.comp.Resolve,用來檢查變數,方法或者類的訪問是否合法,變數是否是靜態變數
  • com.sun.tools.javac.comp.ConstFold,將一個字串常量中的多個字元合併成一個字串
  • com.sun.tools.javac.comp.Infer,幫助推導泛型方法的引數型別

3.4舉例

變數自動轉化

public class Yuyi{
    public static void main(String agrs[]){
        Integer i = 1;
        Long l = i + 2L;
        System.out.println(l);
    }
}
//經過自動轉換後
public class Yuyi{
    public Yuyi(){
        super();
    }
    public static void main(String agrs[]){
        Integer i = Integer.valueOf(1);
        Long l = Long.valueOf(i.intValue() + 2L);
        System.out.println(l);
    }
}
複製程式碼

解除語法糖

public class Yuyi{
    public static void main(String agrs[]){
        int[] array = {1,2,3};
        for (int i : array){
            System.out.println(i);
        }
    }
}
//解除語法糖後
public class Yuyi{
    public Yuyi(){
        super();
    }
    public static void main(String agrs[]){
        int[] arrays = {1,2,3};
        for (int[] arr$ = array,len$=arr$.length,i$=0; i$<len$; ++i$){
            int i = arr$[i$];
            {
                System.out.println(i);
            }
        }
    }
}
複製程式碼

內部類解析

public class Yuyi{
    public static void main(String agrs[]){
        Inner inner = new Inner();
        inner.print();
    }
    class Inner{
        public void print(){
            System.out.println("Yuyi$Inner.print");
        }
    }
}
//轉化後的程式碼如下
public class Yuyi{
    public Yuyi(){
        super();
    }
    public static void main(String agrs[]){
        Yuyi$Inner inner = new Yuyi$Inner(this);
        inner.print();
    }
    {
    }
}
class Yuyi$Inner{
    /*synthetic*/ final Yuyi this$0;
    
    Yuyi$Inner(/*synthetic*/final Yuyi this$0){
        this.this$0 = this$0;
        super();
    }
    
    public void print(){
        System.out.println("Yuyi$Inner.print");
    }
}

複製程式碼

4.程式碼生成器

4.1作用

生成語法樹後,接下來Javac會呼叫com.sun.tools.javac.jvm.Gen類遍歷語法樹,生成Java位元組碼

4.2步驟

  • 將java方法中程式碼塊轉化為符合JVM語法的命令形式,JVM的操作都是基於棧的,所有的操作都必須經過出棧和進棧來完成
  • 按照JVM的檔案組織格式將位元組碼輸出到以class為擴充名的檔案中

4.3所用類庫

  • com.sun.tools.javac.jvm.Gen類,用來遍歷語法樹,生成最終Java位元組碼
  • com.sun.tools.javac.jvm.Items,輔助gen,這個類表示任何可定址的操作項,這些操作項都可以作為一個單位出現在操作棧上
  • com.sun.tools.javac.jvm.Code,輔助gen,儲存生成的位元組碼,並提供一些能夠對映操作碼的方法

參考書籍:《深入分析Java Web》

相關文章