Javac编译原理:基本结构和工作原理

Javac编译器

文章目录

  • Javac编译器
    • 简介
    • 基本结构
      • 如何编译程序
    • 工作原理
      • 词法分析器
      • 语法分析器
      • 语义分析器
      • 代码生成器

简介

javac是一种编译器,能将一种语言规范转化成另一种语言规范

编译器通常是将便于人理解的语言规范转换成容易理解的语言规范,如C都是将源码直接编译成目标机器码,这个目标机器码是CPU直接执行的指令集合,这些指令集合也就是底层的一种语言规范,机器能够直接识别这种语言规范,虽然这种机器码执行起来高效,但是对人不友好,开发这个代码的成本远远高于省下的机器的执行成本

从某种程度上来说,有了编译器才有了程序语言的繁荣,编译器是人类和机器沟通的一个纽带

javac的任务:将Java源代码语言先转换成JVM能识别的一种语言,再由JVM将JVM语言转化成当前机器能识别的机器语言

Java语言的执行和平台无关,也成就了Java语言的繁荣

Javac编译原理:基本结构和工作原理_第1张图片

从图中可以看出,Javac的任务就是将Java源码编译成Java字节码,也就是JVM能够识别的二进制码,(.java - .class),这些二进制数字是有格式的,只有JVM能正确识别

基本结构

Javac编译器的作用:将符合Java语言规范的源代码转化成符合Java虚拟机规范的Java字节码

如何编译程序

Javac主要四个模块:词法分析器、语法分析器、语义分析器和代码生成器

词法分析:一个字节为一节的读,找出语法关键词,最终从源代码中找出一些规范化的Token流

语法分析:检查关键词组合在一起是否符合Java语言规范,形成一个符合Java语法规范的抽象语法树,抽象语法树是一个结构化的语法表达形式,作用:把语言的主要词法用一个结构化的形式组织在一起,这颗语法树在之后可以按照新的规则重新组织,也是编译器的关键所在

语义分析:把一些难懂的、复杂的语法转换成更加简单的语法,结果就是将复杂语法转换成简单语法,还有注解,形成一个注解过后的抽象语法树,这棵语法树更加接近目标语言的语法规则

代码生成:通过字节码生成器生成字节码,根据经过注解的抽象语法树生成字节码,也就是将一个数据结构转换成另一个数据结构,代码生成器的结果就是生成符合Java虚拟机规范的字节码

Javac编译原理:基本结构和工作原理_第2张图片

工作原理

词法分析器

作用:将Java源文件的字符流转变成对应的Token流

类结构

Javac主要词法分析器的接口类是

package com.sun.tools.javac.parser;
public interface Lexer {

它的默认实现类是

package com.sun.tools.javac.parser;
public class Scanner implements Lexer {

Scanner会逐个读取Java源文件的单个字符,然后解析出符合Java语言规范的Token序列,所涉及的类:

Javac编译原理:基本结构和工作原理_第3张图片

由Factory生成了两个接口的实现类:Scanner和JavacParser,这两个类负责整个词法分析的过程控制;

JavacParser:规定哪些词是符合Java语言规范规定的

Scanner:读取和归类不同词法的操作

Token:规定了所有Java语言的合法关键词

Names:存储和表示解析后的词法

词法分析过程是在JavacParser的该方法中完成的:

    /** CompilationUnit = [ { "@" Annotation } PACKAGE Qualident ";"] {ImportDeclaration} {TypeDeclaration}
     */
    public JCTree.JCCompilationUnit parseCompilationUnit() {
        Token firstToken = token;
        JCModifiers mods = null;
        boolean consumedToplevelDoc = false;
        boolean seenImport = false;
        boolean seenPackage = false;
        ListBuffer<JCTree> defs = new ListBuffer<>();
        //解析修饰符
        if (token.kind == MONKEYS_AT)
            mods = modifiersOpt(); 

        //解析package声明
        if (token.kind == PACKAGE) { 
            int packagePos = token.pos;
            List<JCAnnotation> annotations = List.nil();
            seenPackage = true;
            if (mods != null) {
                checkNoMods(mods.flags & ~Flags.DEPRECATED);
                annotations = mods.annotations;
                mods = null;
            }
            nextToken();
            JCExpression pid = qualident(false);
            accept(SEMI);
            JCPackageDecl pd = toP(F.at(packagePos).PackageDecl(annotations, pid));
            attach(pd, firstToken.comment(CommentStyle.JAVADOC));
            consumedToplevelDoc = true;
            defs.append(pd);
        }

        boolean checkForImports = true;
        boolean firstTypeDecl = true;
        while (token.kind != EOF) {
            if (token.pos <= endPosTable.errorEndPos) {
                // error recovery 跳过错误字符
                skip(checkForImports, false, false, false);
                if (token.kind == EOF)
                    break;
            }
            if (checkForImports && mods == null && token.kind == IMPORT) {
                seenImport = true;
                //解析import声明
                defs.append(importDeclaration());
            } else {
                Comment docComment = token.comment(CommentStyle.JAVADOC);
                if (firstTypeDecl && !seenImport && !seenPackage) {
                    docComment = firstToken.comment(CommentStyle.JAVADOC);
                    consumedToplevelDoc = true;
                }
                //SEMI(";"),
                if (mods != null || token.kind != SEMI)
                    mods = modifiersOpt(mods);
                if (firstTypeDecl && token.kind == IDENTIFIER) {
                    ModuleKind kind = ModuleKind.STRONG;
                    if (token.name() == names.open) {
                        kind = ModuleKind.OPEN;
                        nextToken();
                    }
                    //Token.INDENTIFIER 用于表示用户定义的名称
                    if (token.kind == IDENTIFIER && token.name() == names.module) {
                        if (mods != null) {
                            checkNoMods(mods.flags & ~Flags.DEPRECATED);
                        }
                        defs.append(moduleDecl(mods, kind, docComment));
                        consumedToplevelDoc = true;
                        break;
                    } else if (kind != ModuleKind.STRONG) {
                        reportSyntaxError(token.pos, Errors.ExpectedModule);
                    }
                }
                JCTree def = typeDeclaration(mods, docComment);
                if (def instanceof JCExpressionStatement statement)
                    def = statement.expr;
                defs.append(def);
                if (def instanceof JCClassDecl)
                    checkForImports = false;
                mods = null;
                firstTypeDecl = false;
            }
        }
        JCTree.JCCompilationUnit toplevel = F.at(firstToken.pos).TopLevel(defs.toList());
        if (!consumedToplevelDoc)
            attach(toplevel, firstToken.comment(CommentStyle.JAVADOC));
        if (defs.isEmpty())
            storeEnd(toplevel, S.prevToken().endPos);
        if (keepDocComments)
            toplevel.docComments = docComments;
        if (keepLineMap)
            toplevel.lineMap = S.getLineMap();
        this.endPosTable.setParser(null); // remove reference to parser
        toplevel.endPositions = this.endPosTable;
        return toplevel;
    }

从源文件的一个字符开始,按照Java语法规范依次找出package、import、类定义、属性和方法定义,最后构建一个抽象语法树

Javac是如何分辨一个token的呢?

    /** The factory to be used for abstract syntax tree construction.
     */
    protected TreeMaker F;
	/**
     * Qualident = Ident { DOT [Annotations] Ident }
     */
    public JCExpression qualident(boolean allowAnnos) {
        JCExpression t = toP(F.at(token.pos).Ident(ident()));
        while (token.kind == DOT) { //判断这个token是否token.DOT,如果是则读取整个package定义的类名
            int pos = token.pos;
            nextToken();
            List<JCAnnotation> tyannos = null;
            if (allowAnnos) {
                //注解
                tyannos = typeAnnotationsOpt();
            }
            t = toP(F.at(pos).Select(t, ident()));
            if (tyannos != null && tyannos.nonEmpty()) {
                t = toP(F.at(tyannos.head.pos).AnnotatedType(tyannos, t));
            }
        }
        return t;
    }

先根据Token.INDENTIFIER 的token创建一个JCIdent的语法节点,然后取下一个token,如果为DOT,则进入while循环读取整个路径

如何判断哪些字符组合是一个token则是在Scanner类中定义:

//每调用依次方法就会形成一个token    
public void nextToken() {
        prevToken = token;
        if (!savedTokens.isEmpty()) {
            token = savedTokens.remove(0);
        } else {
            token = tokenizer.readToken();
        }
    }

实际上在读取每个token时都需要一个转换过程,在java源码中的所有字符集合都要找到在com.sun.tools.javac.parser.INDENTIFIER中定义的对应关系,这个任务则是在com.sun.tools.javac.parser.Keywords中完成,Kaywords负责将所有字符集合对应到token集合中

字符集合到token转换相关的类关系:

Javac编译原理:基本结构和工作原理_第4张图片

每个字符集合都是一个Name对象,所有的Name对象都存储在Name.Table这个内部类,这个类也就是对应的这个类的符号表,会将所有的元素按照他们的Token.name转化成name对象,然后建立Name对象和Token的对应关系,保存在Keyworks里的key数组,其他的所有字符集合则会被对应到Token.INDENTIFIER类型

Javac编译原理:基本结构和工作原理_第5张图片

语法分析器

作用:将Token流组建成更加结构化的语法树

语法树的规则

每个语法节点都会实现一个接口xxxTree,这个接口继承自com.sun.source.tree.Tree接口,例如IfTree语法节点表示一个if类型的表达式

每个语法节点都是com.sun.tools.javac.tree.JCTree的子类,并且会实现第一节点中的xxxTree接口类,这个类的名称类似于JCxxx

所有JCxxx 类都作为一个静态内部类定义在JCTree类中

Javac编译原理:基本结构和工作原理_第6张图片

JCTree类有三个重要的属性类

Tree tag :每个语法节点都会用一个整型常数表示,并且每个节点类型的数值都是在前一个的基础上+1,顶层节点TOPLEVEL是1,而IMPORT节点等于TOPLEVEL+1,也就是2

pos :也是一个整数,存储的是这个语法节点在源代码中的起始位置,一个文件的位置是0,-1代表不存在

type :表示这个节点是什么Java类型

例如之前的这个函数

public JCExpression qualident(boolean allowAnnos) {
        JCExpression t = toP(F.at(token.pos).Ident(ident()));
    ...
}

调用了TreeMaker类,根据Name对象构建了一个JCIdent语法节点,如果包名是多级目录,将构建成JCFieldAccess语法节点,此节点也可以是嵌套关系

Package 节点解析完成之后进入while循环,首先解析importDeclaration,解析规则和package类似;解析节点之后构建语法树:

    /** ImportDeclaration = IMPORT [ STATIC ] Ident { "." Ident } [ "." "*" ] ";"
     */
    protected JCTree importDeclaration() {
        int pos = token.pos;
        nextToken();
        boolean importStatic = false;
        //检查是否有static关键字,如果有则设置标识,然后解析第一个类路径,是多级目录则继续读取下一个,并构建JCFiledAccess
        if (token.kind == STATIC) {
            importStatic = true;
            nextToken();
        }
        JCExpression pid = toP(F.at(token.pos).Ident(ident()));
        do {
            int pos1 = token.pos;
            accept(DOT);
            //如果最后一个Token为*,则设置这个JCFieldAccess的Token名称为asterisk
            if (token.kind == STAR) {
                pid = to(F.at(pos1).Select(pid, names.asterisk));
                nextToken();
                break;
            } else {
                pid = toP(F.at(pos1).Select(pid, ident()));
            }
        } while (token.kind == DOT);
        accept(SEMI);
        //最后将这个解析的语法节点作为子结点构建在新创建的JCImport节点中
        return toP(F.at(pos).Import(pid, importStatic));
    }

JCImport语法树如图:

Javac编译原理:基本结构和工作原理_第7张图片

Import节点解析完成之后就是class的解析,类包括interface、class、enum,以class为例:

    /** ClassDeclaration = CLASS Ident TypeParametersOpt [EXTENDS Type]
     *                     [IMPLEMENTS TypeList] ClassBody
     *  @param mods    The modifiers starting the class declaration
     *  @param dc       The documentation comment for the class, or null.
     */
    protected JCClassDecl classDeclaration(JCModifiers mods, Comment dc) {
    	//第一个token是这个类的关键词
        int pos = token.pos;
        accept(CLASS);
        Name name = typeName();
//这个类的类型可选参数,将这个参数解析为JCTypeParameter语法节点
        List<JCTypeParameter> typarams = typeParametersOpt();

        JCExpression extending = null;
        if (token.kind == EXTENDS) {
            nextToken();
            extending = parseType();
        }
        List<JCExpression> implementing = List.nil();
        if (token.kind == IMPLEMENTS) {
            nextToken();
            implementing = typeList();
        }
        //对classBody的解析,也是按照变量定义解析、方法定义解析和内部类定义解析,结果保存在list集合
        List<JCExpression> permitting = permitsClause(mods, "class");
        List<JCTree> defs = classInterfaceOrRecordBody(name, false, false);
//最后将这些子节点添加到JCClassDecl这课class树种
        JCClassDecl result = toP(F.at(pos).ClassDef(
            mods, name, typarams, extending, implementing, permitting, defs));
        attach(result, dc);
        return result;
    }

例如这一段代码:

public class YuFa {
    int a;
    private int c = a + 1;
    
    public int getC() {
        return c;
    }
    
    public void setC(int c) {
        this.c = c;
    }
}

这段代码对应的语法树:

Javac编译原理:基本结构和工作原理_第8张图片

当这个类解析完成之后,会将这个类节点加到这个类对应的包路径的顶层节点也就是JCCompilationUnit,它持有以package 作为pid和JCClassDecl 的集合,这样整个.java文件就被解析完成

所举例代码对应的完整语法树:

Javac编译原理:基本结构和工作原理_第9张图片

注意:所有语法节点的生成都是在TreeMaker类中完成,它实现了在JCTree.Factory 接口中定义的所有节点的构成方法

语义分析器

作用:处理语法树,例如添加默认的构造函数

类:com.sun.tools.javac.comp.Enter

主要完成以下两个步骤

将所有类中出现的符号输入到类自身的符号表中,所有类符号、类的参数类型符号(泛型参数类型)、超类符号和继承的接口类型符号等都存储到一个未处理的列表中

将这个未处理列表中所有的类都解析到各自的类符号列表中,这个操作是在MemberEnter.complete()中完成的

下一个步骤是处理annotation,这个步骤是由com.sun.tools.processing.JavacProssessingEnvironment完成的

再接下来是com.sun.tools.javac.comp.Attr,最重要的是检查语义的合法性并进行逻辑判断,如以下几点:

  • 变量的类型是否匹配
  • 变量在使用前是否已经完成初始化
  • 能够推导出泛型方法的参数类型
  • 字符串常量的合并

在这个步骤中除 Atr 之外还需要另外一些类来协助,如下所述。

  • com.sun.tools.javac.comp.Check:辅助 Attr 类检查语法树中的变量类型是否正确,如二元操作符两边的操作数的类型是否匹配,方法返回的类型是否与接收的引用值类型匹配等。
  • com.sun.tools.javac.comp. Resolve: 主要检查变量、方法或者类的访问是否合法、变量是否是静态变量、变量是否已经初始化等。
  • com.sun.tools.javac.comp.ConstFold:常量折叠,这里主要针对字符串常量,会将一个字符串常量中的多个宇符串合并成一个字符串。
  • com.sun.tools.javac.comp.Infer:帮助推导泛型方法的参数类型等

标注完成后由 com.sun.tools.javac.comp.Flow 类完成数据流分析,数据流分析主要完成如下工作:

  1. 检查变量在使用前是否都己经被正确赋值。除了 Java 中的原始类型,如 int.long、byte、double、char、float,都会有默认的初始化值,其他像String 类型和对象的引用都必须在使用前先赋值。
  2. 保证 final 修饰的变量不会被重复赋值。经过final 修饰的变量只能赋一次值,重复赋值会在这一步编译时报错,如果这个变量是静态变量,则在定义时就必须对其赋值。
  3. 要确定方法的返回值类型。这里需要检查方法的返回值类型是否确定,并检查接受这个方法返回值的引用类型是否匹配,如果没有返回值,则不能有任何引用类型指向方法的这个返回值。
  4. 所有的 Checked Exception 都要捕获或者向上抛出。例如,我们使用 FilelnputStream读取一个文件时,必须捕获可能抛出的FileNotFondException 异常,或者直接向上层方法抛出这个异常。
  5. 所有的语句都要被执行到。这里会检查是否有语句出现在一个return 方法的后面,因为在 return 方法后面的语句永远也不会被执行到。

语法分析的最后一步是执行com.sun.tools.javac.comp.Flow,这是在进一步对语法树进行语义分析,如消除一些无用的代码,总结:

  • 去掉无用的代码
  • 变量的自动转换
  • 去除语法糖

代码生成器

经过语义分析器完成后的语法树已经非常完善了,接下来javac会调用com.sun.tools.javac.jvm.Gen类遍历语法树,生成最终的Java字节码,主要为两个步骤:

将Java方法中的代码块转化成符合JVM语法的命令形式,JVM的操作是基于栈的,所有操作都必须经过出栈和进栈完成

按照JVM的文件组织格式将字节码输出到以class为拓展名的文件

生成字节码除Gen类之外还有两个重要的辅助类:

Items:这个类表示任何可寻址的操作项,包括本地变量、类实例变量或者常量池中用户自定义的常量,这些操作项都可以作为一个单位出现在操作栈上

不同类型的Item对应不同的JVM的操作码:ImmediateItem(常量类型)、LocalItem(本地变量)、StackItem(栈中元素)

Code:存储生成的字节码,并提供一些能够映射操作码的方法

Gen会以后序遍历的顺序解析语法树,将add方法的方法块JCBlock的代码转换成JVM对应的字节码,时序图:

Javac编译原理:基本结构和工作原理_第10张图片

最后使用callMethod方法,返回给方法的调用者

你可能感兴趣的:(Java,SE,java,jvm,开发语言)