淺談Javac編譯原理

Javac就是java編譯器,它的作用就是把java源代碼轉化為JVM能識別的一種語言,然后JVM可以將這種語言轉為當前運行機器所能識別的機器碼,從而執行程序。這篇文章只談源代碼到jvm的字節碼的過程。

Javac使源碼轉為JVM字節碼需要經歷4個過程:詞法分析,語法分析,語義分析,代碼生成。
本篇文章以jdk1.7版本及以下講解,1.8后編譯相關的源碼改動較大,具體變化挖坑以后再補。

詞法分析

Javac的主要詞法分析器的接口類是com.sun.tools.javac.parser.Lexer,它的默認實現類是com.sun.tools.javac.parser.Scanner,Scanner會逐個讀取Java源文件的單個字符,然后解析出符合Java語言規范的Token序列。

public enum Token {
    EOF,
    ERROR,
    IDENTIFIER,
    ABSTRACT("abstract"),
    ASSERT("assert"),
    BOOLEAN("boolean"),
    BREAK("break"),
    BYTE("byte"),
    CASE("case"),
    CATCH("catch"),
    CHAR("char"),
    CLASS("class"),
    CONST("const"),
    CONTINUE("continue"),
    DEFAULT("default"),
    DO("do"),
    DOUBLE("double"),
    ELSE("else"),
    ENUM("enum"),
    EXTENDS("extends"),
    FINAL("final"),
    FINALLY("finally"),
    FLOAT("float"),
    FOR("for"),
    GOTO("goto"),
    IF("if"),
    IMPLEMENTS("implements"),
    IMPORT("import"),
    INSTANCEOF("instanceof"),
    INT("int"),
    INTERFACE("interface"),
    LONG("long"),
    NATIVE("native"),
    NEW("new"),
    PACKAGE("package"),
    PRIVATE("private"),
    PROTECTED("protected"),
    PUBLIC("public"),
    RETURN("return"),
    SHORT("short"),
    STATIC("static"),
    STRICTFP("strictfp"),
    SUPER("super"),
    SWITCH("switch"),
    SYNCHRONIZED("synchronized"),
    THIS("this"),
    THROW("throw"),
    THROWS("throws"),
    TRANSIENT("transient"),
    TRY("try"),
    VOID("void"),
    VOLATILE("volatile"),
    WHILE("while"),
    INTLITERAL,
    LONGLITERAL,
    FLOATLITERAL,
    DOUBLELITERAL,
    CHARLITERAL,
    STRINGLITERAL,
    TRUE("true"),
    FALSE("false"),
    NULL("null"),
    LPAREN("("),
    RPAREN(")"),
    LBRACE("{"),
    RBRACE("}"),
    LBRACKET("["),
    RBRACKET("]"),
    SEMI(";"),
    COMMA(","),
    DOT("."),
    ELLIPSIS("..."),
    EQ("="),
    GT(">"),
    LT("<"),
    BANG("!"),
    TILDE("~"),
    QUES("?"),
    COLON(":"),
    EQEQ("=="),
    LTEQ("<="),
    GTEQ(">="),
    BANGEQ("!="),
    AMPAMP("&&"),
    BARBAR("||"),
    PLUSPLUS("++"),
    SUBSUB("--"),
    PLUS("+"),
    SUB("-"),
    STAR("*"),
    SLASH("/"),
    AMP("&"),
    BAR("|"),
    CARET("^"),
    PERCENT("%"),
    LTLT("<<"),
    GTGT(">>"),
    GTGTGT(">>>"),
    PLUSEQ("+="),
    SUBEQ("-="),
    STAREQ("*="),
    SLASHEQ("/="),
    AMPEQ("&="),
    BAREQ("|="),
    CARETEQ("^="),
    PERCENTEQ("%="),
    LTLTEQ("<<="),
    GTGTEQ(">>="),
    GTGTGTEQ(">>>="),
    MONKEYS_AT("@"),
    CUSTOM;
}

Token是一個枚舉類,定義了java語言中的系統關鍵字和符號,Token. IDENTIFIER用于表示用戶定義的名稱,如類名、包名、變量名、方法名等。

這里有兩個問題,Javac是如何分辨這一個個Token的呢?例如,它是怎么知道package就是一個Token.PACKAGE,而不是用戶自定義的Token.INENTIFIER的名稱呢。另一個問題是,Javac是如何知道哪些字符組合在一起就是一個Token的呢?

答案1:Javac在進行詞法分析時會由JavacParser根據Java語言規范來控制什么順序、什么地方應該出現什么Token,Token流的順序要符合Java語言規范。如package這個關鍵詞后面必然要跟著用戶定義的變量表示符,在每個變量表示符之間必須用“.”分隔,結束時必須跟一個“;”。
下圖是讀取Token流程


QQ圖片20180721132355.png

答案2:如何判斷哪些字符組合是一個Token的規則是在Scanner的nextToken方法中定義的,每調用一次這個方法就會構造一個Token,而這些Token必然是com.sun.tools.javac.parser.Token中的任何元素之一。以下為源碼:

public void nextToken() {
        try {
            this.prevEndPos = this.endPos;
            this.sp = 0;

            while(true) {
                this.pos = this.bp;
                switch(this.ch) {
                case '\t':
                case '\f':
                case ' ':
                    do {
                        do {
                            this.scanChar();
                        } while(this.ch == 32);
                    } while(this.ch == 9 || this.ch == 12);

                    this.endPos = this.bp;
                    this.processWhiteSpace();
                    break;
                case '\n':
                    this.scanChar();
                    this.endPos = this.bp;
                    this.processLineTerminator();
                    break;
                case '\u000b':
                case '\u000e':
                case '\u000f':
                case '\u0010':
                case '\u0011':
                case '\u0012':
                case '\u0013':
                case '\u0014':
                case '\u0015':
                case '\u0016':
                case '\u0017':
                case '\u0018':
                case '\u0019':
                case '\u001a':
                case '\u001b':
                case '\u001c':
                case '\u001d':
                case '\u001e':
                case '\u001f':
                case '!':
                case '#':
                case '%':
                case '&':
                case '*':
                case '+':
                case '-':
                case ':':
                case '<':
                case '=':
                case '>':
                case '?':
                case '@':
                case '\\':
                case '^':
                case '`':
                case '|':
                default:
                    if(this.isSpecial(this.ch)) {
                        this.scanOperator();
                        return;
                    } else {
                        boolean var6;
                        if(this.ch < 128) {
                            var6 = false;
                        } else {
                            char var2 = this.scanSurrogates();
                            if(var2 != 0) {
                                if(this.sp == this.sbuf.length) {
                                    this.putChar(var2);
                                } else {
                                    this.sbuf[this.sp++] = var2;
                                }

                                var6 = Character.isJavaIdentifierStart(Character.toCodePoint(var2, this.ch));
                            } else {
                                var6 = Character.isJavaIdentifierStart(this.ch);
                            }
                        }

                        if(var6) {
                            this.scanIdent();
                            return;
                        } else {
                            if(this.bp != this.buflen && (this.ch != 26 || this.bp + 1 != this.buflen)) {
                                this.lexError("illegal.char", new Object[]{String.valueOf(this.ch)});
                                this.scanChar();
                            } else {
                                this.token = Token.EOF;
                                this.pos = this.bp = this.eofPos;
                            }

                            return;
                        }
                    }
                case '\r':
                    this.scanChar();
                    if(this.ch == 10) {
                        this.scanChar();
                    }

                    this.endPos = this.bp;
                    this.processLineTerminator();
                    break;
                case '\"':
                    this.scanChar();

                    while(this.ch != 34 && this.ch != 13 && this.ch != 10 && this.bp < this.buflen) {
                        this.scanLitChar();
                    }

                    if(this.ch == 34) {
                        this.token = Token.STRINGLITERAL;
                        this.scanChar();
                    } else {
                        this.lexError(this.pos, "unclosed.str.lit", new Object[0]);
                    }

                    return;
                case '$':
                case 'A':
                case 'B':
                case 'C':
                case 'D':
                case 'E':
                case 'F':
                case 'G':
                case 'H':
                case 'I':
                case 'J':
                case 'K':
                case 'L':
                case 'M':
                case 'N':
                case 'O':
                case 'P':
                case 'Q':
                case 'R':
                case 'S':
                case 'T':
                case 'U':
                case 'V':
                case 'W':
                case 'X':
                case 'Y':
                case 'Z':
                case '_':
                case 'a':
                case 'b':
                case 'c':
                case 'd':
                case 'e':
                case 'f':
                case 'g':
                case 'h':
                case 'i':
                case 'j':
                case 'k':
                case 'l':
                case 'm':
                case 'n':
                case 'o':
                case 'p':
                case 'q':
                case 'r':
                case 's':
                case 't':
                case 'u':
                case 'v':
                case 'w':
                case 'x':
                case 'y':
                case 'z':
                    this.scanIdent();
                    return;
                case '\'':
                    this.scanChar();
                    if(this.ch == 39) {
                        this.lexError("empty.char.lit", new Object[0]);
                        return;
                    } else {
                        if(this.ch == 13 || this.ch == 10) {
                            this.lexError(this.pos, "illegal.line.end.in.char.lit", new Object[0]);
                        }

                        this.scanLitChar();
                        if(this.ch == 39) {
                            this.scanChar();
                            this.token = Token.CHARLITERAL;
                        } else {
                            this.lexError(this.pos, "unclosed.char.lit", new Object[0]);
                        }

                        return;
                    }
                case '(':
                    this.scanChar();
                    this.token = Token.LPAREN;
                    return;
                case ')':
                    this.scanChar();
                    this.token = Token.RPAREN;
                    return;
                case ',':
                    this.scanChar();
                    this.token = Token.COMMA;
                    return;
                case '.':
                    this.scanChar();
                    if(48 <= this.ch && this.ch <= 57) {
                        this.putChar('.');
                        this.scanFractionAndSuffix();
                        return;
                    }

                    if(this.ch == 46) {
                        this.putChar('.');
                        this.putChar('.');
                        this.scanChar();
                        if(this.ch == 46) {
                            this.scanChar();
                            this.putChar('.');
                            this.token = Token.ELLIPSIS;
                        } else {
                            this.lexError("malformed.fp.lit", new Object[0]);
                        }

                        return;
                    } else {
                        this.token = Token.DOT;
                        return;
                    }
                case '/':
                    this.scanChar();
                    if(this.ch != 47) {
                        if(this.ch != 42) {
                            if(this.ch == 61) {
                                this.name = this.names.slashequals;
                                this.token = Token.SLASHEQ;
                                this.scanChar();
                            } else {
                                this.name = this.names.slash;
                                this.token = Token.SLASH;
                            }

                            return;
                        }

                        this.scanChar();
                        Scanner.CommentStyle var1;
                        if(this.ch == 42) {
                            var1 = Scanner.CommentStyle.JAVADOC;
                            this.scanDocComment();
                        } else {
                            var1 = Scanner.CommentStyle.BLOCK;

                            while(this.bp < this.buflen) {
                                if(this.ch == 42) {
                                    this.scanChar();
                                    if(this.ch == 47) {
                                        break;
                                    }
                                } else {
                                    this.scanCommentChar();
                                }
                            }
                        }

                        if(this.ch != 47) {
                            this.lexError("unclosed.comment", new Object[0]);
                            return;
                        }

                        this.scanChar();
                        this.endPos = this.bp;
                        this.processComment(var1);
                    } else {
                        do {
                            this.scanCommentChar();
                        } while(this.ch != 13 && this.ch != 10 && this.bp < this.buflen);

                        if(this.bp < this.buflen) {
                            this.endPos = this.bp;
                            this.processComment(Scanner.CommentStyle.LINE);
                        }
                    }
                    break;
                case '0':
                    this.scanChar();
                    if(this.ch != 120 && this.ch != 88) {
                        this.putChar('0');
                        this.scanNumber(8);
                        return;
                    } else {
                        this.scanChar();
                        if(this.ch == 46) {
                            this.scanHexFractionAndSuffix(false);
                            return;
                        } else {
                            if(this.digit(16) < 0) {
                                this.lexError("invalid.hex.number", new Object[0]);
                            } else {
                                this.scanNumber(16);
                            }

                            return;
                        }
                    }
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    this.scanNumber(10);
                    return;
                case ';':
                    this.scanChar();
                    this.token = Token.SEMI;
                    return;
                case '[':
                    this.scanChar();
                    this.token = Token.LBRACKET;
                    return;
                case ']':
                    this.scanChar();
                    this.token = Token.RBRACKET;
                    return;
                case '{':
                    this.scanChar();
                    this.token = Token.LBRACE;
                    return;
                case '}':
                    this.scanChar();
                    this.token = Token.RBRACE;
                    return;
                }
            }
        } finally {
            this.endPos = this.bp;
            if(scannerDebug) {
                System.out.println("nextToken(" + this.pos + "," + this.endPos + ")=|" + new String(this.getRawCharacters(this.pos, this.endPos)) + "|");
            }

        }
    }

語法分析

語法分析器是將詞法分析器分析的Token流組建成更加結構化的語法樹,也就是將一個個單詞組裝成一句話,一個完整的語句。Javac的語法樹使得Java源碼更加結構化,這種結構化可以為后面的進一步處理提供方便。每個語法樹上的節點都是com.sun.tools.javac.tree.JCTree的一個示例,關于語法樹有以下規則 :
1.每個語法節點都會實現一個接口xxxTree,這個接口又繼承自com.sun.source.tree.Tree接口,如IfTree語法節點表示一個if類型的表達式,BinaryTree語法節點代表一個二元操作表達式。
2.每個語法節點都是com.sun.tools.javac.tree.JCTree的子類,并且會實現第一節點中的xxxTree接口類,這個類的名稱類似于JCxxx,如實現IfTree接口的實現類為JCIf,實現BinaryTree接口的類為JCBinary等。
3.所有的JCxxx類都作為一個靜態內部類定義在JCTree類中。

JCTree類中有如下3個重要的屬性項。
1.Ttree tag:每個語法節點都會用一個整形常數表示,并且每個節點類型的數值是在前一個的基礎上加1。頂層節點TOPLEVEL是1,而IMPORT節點等于TOPLEVEL加1,等于2.
2.pos:也是一個整數,它存儲的是這個語法節點在源代碼中的起始位置,一個文件的位置是0,而-1表示不存在。
3.type:它表示的是這個節點是什么Java類型,如是int、float還是String.

語義分析

在得到結構化可操作的語法樹后,還需要經過語義分析器給這棵語法樹做一些處理,如給類添加默認的構造函數,檢查變量在使用前是否已經初始化,將一些常量合并處理,檢查操作變量類型是否匹配,檢查異常是否已經捕獲或拋出,解除java語法糖等等。
一般有以下幾個步驟:
1.將Java類中的符號輸入到符號表。主要由com.sun.tools.javac.comp.Enter類來完成,首先把所有類中出現的符號輸入到類自身的符號表中,所有類符號、類的參數類型符號、超類符號和繼承的接口類型符號都存著到一個未處理的列表中,然后在MemberEnter.completer()方法中獎未處理列表中所有類都解析到各自的類符號表中。Enter類解析中會給類添加默認構造函數。
2.處理注解,由com.sun.tools.javac.processing.JavacProcessingEnvironment類完成
3.進行標注com.sun.tools.javac.comp.Attr,檢查語義的合法性并進行邏輯判斷。如變量的類型是否匹配,使用前是否已經初始化等。
4.進行數據流分析,檢查變量在使用前是否已經被正確賦值,保證final修飾變量不會被重復賦值,確定方法的返回值類型,異常需要被捕獲或者拋出,所有的語句都要被執行到(指檢查是否有語句出現在return方法的后面)
5.執行com.suntools.javac.comp.Flow,可以總結為去掉無用的代碼,如用假的if代碼塊;變量的自動轉換;去除語法糖,如foreach變成普通for循環。

代碼生成器

把修飾后的語法樹生成最終的Java字節碼,通過com.sun.tools.javac.jvm.Gen類遍歷語法樹來生成。有以下兩個步驟:
1.將Java方法中的代碼塊轉化成符合JVM語法的命令形式,JVM的操作都是基于棧的,所有的操作都必須經過出棧和進棧來完成。
2.按照JVM的文件組裝格式講字節碼輸出到以class為擴展名的文件中
這里還有兩個輔助類:
1.Items,這個類表示任何可尋址的操作項,包括本地變量、類實例變量或者常量池中用戶自定義的常量等。
2.Code,存儲生成的字節碼,并提供一些能夠映射操作碼的方法。
示例說明:

public class Daima{
  public static void main(String[] args){
    int rt = add(1,2);
}

public static int add(Integer a, Integer b){
  return a+b;
}
}

重點說明add方法是如何轉成字節碼,這個方法中有一個加法表達式,JVM是基于棧來操作數值的,所以要執行一個二元操作,必須將兩個數值a和b放到操作棧,然后利用加法操作符執行加法操作,將加法的結果放到當前棧的棧項,最后將這個結果返回給調用者。

一張圖總結Javac編譯過程:


QQ圖片20180722232142.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容

  • 本文基于周志明的《深入理解java虛擬機 JVM高級特性與最佳實踐》所寫。特此推薦。 列舉了這3類編譯過程中一些比...
    陽光的技術小棧閱讀 427評論 0 0
  • 本文涉及的javac編譯器來自openjdk. javac的目錄地址為: 解壓目錄/langtools/src/s...
    whthomas閱讀 1,393評論 3 3
  • 《愛的五種語言》 P66-67 精心的會話 請寫出你的: 【I】原文拆解 在人際溝通中,我們擅長思考和講話,應對...
    小二翻身做掌柜閱讀 230評論 1 0
  • 有時會被失敗主義這個惡棍死死掐住脖子,覺得無力反抗,心想,快被掐死算了。可沒到快要死透時,又掙扎著反抗。一次又一次...
    勒尤閱讀 154評論 0 0
  • 我把所有的悲觀折疊 丟在墻角 在晨曦下閉目托腮 讓風帶走憂郁 讓陽光驅散陰霾 你真的不知道 生活在下一秒會帶來什么...
    你與時光同行閱讀 203評論 0 0