浅谈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

你可能感兴趣的:(浅谈Javac编译原理)