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流程
答案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編譯過程: