浅谈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阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 本文基于周志明的《深入理解java虚拟机 JVM高级特性与最佳实践》所写。特此推荐。 列举了这3类编译过程中一些比...
    阳光的技术小栈阅读 417评论 0 0
  • 本文涉及的javac编译器来自openjdk. javac的目录地址为: 解压目录/langtools/src/s...
    whthomas阅读 1,364评论 3 3
  • 《爱的五种语言》 P66-67 精心的会话 请写出你的: 【I】原文拆解 在人际沟通中,我们擅长思考和讲话,应对...
    小二翻身做掌柜阅读 222评论 1 0
  • 有时会被失败主义这个恶棍死死掐住脖子,觉得无力反抗,心想,快被掐死算了。可没到快要死透时,又挣扎着反抗。一次又一次...
    勒尤阅读 151评论 0 0
  • 我把所有的悲观折叠 丢在墙角 在晨曦下闭目托腮 让风带走忧郁 让阳光驱散阴霾 你真的不知道 生活在下一秒会带来什么...
    你与时光同行阅读 195评论 0 0