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编译过程: