失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > java 源码分析_Java 源代码编译成 Class 文件的过程分析

java 源码分析_Java 源代码编译成 Class 文件的过程分析

时间:2021-01-28 07:51:07

相关推荐

java 源码分析_Java 源代码编译成 Class 文件的过程分析

原标题:Java 源代码编译成 Class 文件的过程分析

在上篇文章《 Java三种编译方式:前端编译 JIT编译 AOT编译 》中了解到了它们各有什么优点和缺点,以及前端编译+JIT编译方式的运作过程。

下面我们详细了解Java前端编译:Java源代码编译成Class文件的过程;我们从官方JDK提供的前端编译器javac入手,用javac编译一些测试程序,调试跟踪javac源码,看看javac整个编译过程是如何实现的。 1、javac编译器 1-1、javac源码与调试

javac编译器是官方JDK中提供的前端编译器,JDK/bin目录下的javac只是一个与平台相关的调用入口,具体实现在JDK/lib目录下的tools.jar。此外,JDK6开始提供在运行时进行前端编译,默认也是调用到javac,如图:

javac是由Java语言编写的,而HotSpot虚拟机则是由C++语言编写;标准JDK中并没有提供javac的源码,而在OpenJDK中的提供;我们需要在Eclipse中调试跟踪javac源码,看整个编译过程是如何实现的。

javac编译器源码下载(JDK8):http://hg./jdk8u/jdk8u-dev/langtools/archive/tip.tar.bz2

javac编译器源码目录:**\src\share\classes\com\sun\tools\javac

在Eclipse新建工程导入后,可以看到javac源码的目录结构如下:

javac编译器程序入口:com.sun.tools.javac.Main类中的main()方法;

运行javac程序,先是解析命令行参数,由com.sun.tools.javac.pile()方法处理,代码片段如下:

因为没有给参数,可看到输出的是javac用法,如下:

这就是平时我们用JDK/bin/javac的用法,更多javac选项用法请参考:/javase/8/docs/technotes/tools/unix/javac.html

调试编译文件,需要右键工程 -> Debug As -> Debug Configurations ->切换到Arguments选项卡,在Program arguments中输入我们要用javac编译的Java程序文件的路径即可;然后就可以打断点Debug运行调试了,如图:

1-2、javac编译过程

JVM规范定义了Class文件结构格式,但没有定义如何从java程序文件转化为Class文件,所以不同编译器可以有不同实现。

从javac编译器源码来看,其编译过程可以分为3个子过程:

1、解析与填充符号表过程:解析主要包括词法分析和语法分析两个过程;

2、插入式注解处理器的注解处理过程;

3、语义分析与字节码的生成过程;

如图所示(来自参考4):

javac编译动作入口: com.sun.tools.javac.main.JavaCompiler类;

3个编译过程逻辑集中在这个类的compile()和compile2()方法;

如图所示:

1-3、javac中的访问者模式

访问者模式可以将数据结构和对数据结构的操作解耦,使得增加对数据结构的操作不需要修改数据结构,也不必修改原有的操作,而执行时再定义新的Visitor实现者就行了。

Javac经过第一步解析(词法分析和语法分析),会生成用来一棵描述程序代码语法结构的抽象语法树,每个节点都代表程序代码中的一个语法结构,包括:包、类型、修饰符、运算符、接口、返回值、甚至注释等;而后的不同编译阶段都定义了不同的访问者去处理该语法树(节点)。

了解这些更容易理解javac的编译过程实现,而后面分析过程中会再对访问者模式的实现作相关说明。 2、解析与填充符号表 2-1、解析:词法、语法分析

解析包括:词法分析和语法分析两个过程; 2-1-1、词法分析

1、概念解理

词法分析是将源代码的字符流转变为标记(Token)集合;

标记:

标记是编译过程的最小元素;

包括关键字、变量名、字面量、运算符(甚至一个”.”)等;

2、源码分析:

由com.sun.tools.javac.parser.Scanner类实现对外部提供服务;

由com.sun.tools.javac.parser.JavaTokenizer类实现具体的Token分析动作(JavaTokenizer.readToken()方法);

Scanner.nextToken()调用JavaTokenizer.readToken()方法读取下一个Token;

返回com.sun.tools.javac.parser.Tokens.Token类实例表示的一个Token;

Scanner.nextToken()方法如下:

注意,下面语法分析时才会不断调用Scanner.nextToken()读取一个个Token进来解析。 2-1-2、语法分析

1、概念解理

语法分析是根据Token序列构造抽象语法树的过程;

抽象语法树(Abstract Syntax Tree,AST):

是一种用来描述程序代码语法结构的树形表示方式;

每个节点都代表程序代码中的一个语法结构;

语法结构(Construct)包括:包、类型、修饰符、运算符、接口、返回值、甚至注释等;

2、源码分析:

由com.sun.tools.javac.parser.JavacParser类完成整个过程,该类实现com.sun.tools.javac.parser.Parser接口;

一个类文件解析产生的抽象语法树的所有内容保存在JCCompilationUnit类实例里,JCCompilationUnit类是由com.sun.tools.javac.tree.JCTree类扩展;

JCTree是个抽象类,实现了Tree接口,Tree接口里有一个” R accept(TreeVisitor visitor, D data)”方法用来接收访问者,所以Tree接口是访问者模式中的抽象节点元素;

JCTree类中有一个Visitor内部类,同时也是一个抽象类,作为访问者模式中的抽象访问者;

一个JCTree类实例相当于抽象语法树的一个节点,它会扩展许多类型,对应不同语法结构类型的树节点,如JCStatement,JCClassDecl,JCMethodDecl,JCBlock等等,这些类是访问者模式中的具体节点元素;

JCTree扩展的JCMethodDecl方法类型节点结构如下:

代码执行的解析过程,如下:

1)、由pile()方法调用JavaCompiler.parseFiles()方法完成参数输入的所有文件的编译;

2)、JavaCompiler.parseFiles()方法中又调用本类中的parse()方法对其中一个文件进行编译;

该方法中生成JavacParser类实例,然后调用该实例的parseCompilationUnit()方法开始进行整个文件的解析(包括”package”包名),如下: Parser parser = parserFactory.newParser(content, keepComments(), genEndPos, lineDebugInfo); tree = parser.parseCompilationUnit();

返回的tree是JCCompilationUnit类型实例,保存了一个类文件解析产生的抽象语法树的所有内容,也可以说是抽象语法树的根节点;

3)、JavacParser.parseCompilationUnit()方法中调用JavacParser.typeDeclaration()进行文件中所有类型定义的解析;

JavacParser.typeDeclaration()又调用JavacParser.classOrInterfaceOrEnumDeclaration()进行类或接口的解析;

如果是类又调用classDeclaration()对该类进行解析…. JCTree def = typeDeclaration(mods, docComment);

返回一个JCTree类实例表示文件中所有类型定义定义的语法树(不包括”package”包名);

这期间会不断调用Scanner.nextToken()读取一个个Token进来解析;

3、编译测试:

下面我们用javac编译JavacTest.java文件来跟踪整个解析过程,测试文件代码如下: package com.jvmtest; publicclassJavacTest{ privateint i; publicintgetI(){ return i; } publicvoidsetI(int i){ this.i = i; } }

对于解析JavacTest.java文件生成的抽象语法树,由返回的JCCompilationUnit类实例表示,如下图所示:

最外层节点为”com.jvmtest”包名的定义,同时它也是语法树的根节点;

再里一层是”public class JavacTest”类的定义;

再里面可以看到一个字段变量”i”的结构节点,以及两个方法”getI”和”setI”节点;

4、类实例构造函数重名为():

先在再上面的测试程序中加入类实例构造函数: Public JavacTest(){ }

需要注意的是,在classOrInterfaceBodyDeclaration()解析类时,如果遇到添加的类构造函数,会重名为(),如下:

如测试程序中加入类构造函数,可以看到被重命名(),但在生成的树结构上名称还是表现为”JavacTest”,如下

经过上面解析,后续所有操作都建立在抽象语法树之上,下面不会再对源码文件操作; 2-2、填充符号表

1、概念解理

符号表(Symbol Table)是由一组符号地址和符号信息构成的表格,可以想象成哈希表中K-V值的形式;

符号表登记的信息在编译的不同阶段都要用到,如:

1)、用于语义检查和产生中间代码;

2)、在目标代码生成阶段,符号表是对符号名进行地址分配的依据;

2、源码分析:

根据上一步生成的抽象语法树列表,由JavaCompiler.enterTrees()方法完成填充符号表;

由com.sun.p.Enter类实现填充符号表动作,Enter类继承JCTree.Visitor内部抽象类,重写了一些visit**()方法来处理抽象语法树,作为访问者模式中的具体访问者;

符号由com.sun.tools.javac.code.Symbol抽象类表示, 实现了Element接口,Element接口里有一个accept()方法用来接收访问者,所以Element接口是访问者模式中的抽象节点元素;

Symbol类扩展成多种类型的符号,如ClassSymbol表示类的符号、MethodSymbol表示方法的符号等等,这些类是访问者模式中的具体节点元素;

Symbol类和MethodSymbol类定义如下: publicabstractclassSymbolextendsAnnoConstructimplementsElement{ /** The kind of this symbol. * @see Kinds */public int kind; /** The flags of this symbol. */public long flags_field; /** An accessor method for the flags of this symbol. * Flags of class symbols should be accessed through the accessor * method to make sure that the class symbol is loaded. */public long flags() { return flags_field; } /** The name of this symbol in Utf8 representation. */public Name name; /** The type of this symbol. */public Type type; /** The owner of this symbol. */public Symbol owner; /** The completer of this symbol. */public Completer completer; /** A cache for the type erasure of this symbol. */public Type erasure_field; // /** The attributes of this symbol are contained in this * SymbolMetadata. The SymbolMetadata instance is NOT immutable. */protected SymbolMetadata metadata; ...... } /** A class for method symbols. */publicstaticclassMethodSymbolextendsSymbolimplementsExecutableElement{ /** The code of the method. */public Code code = null; /** The extra (synthetic/mandated) parameters of the method. */publicList extraParams = List.nil(); /** The captured local variables in an anonymous class */publicList capturedLocals = List.nil(); /** The parameters of the method. */publicList params = null; /** The names of the parameters */publicList savedParameterNames; /** For an attribute field accessor, its default value if any. * The value is null if none appeared in the method * declaration. */public Attribute defaultValue = null; ...... }

从上面可以看到它们包含了哪些信息;

代码执行的填充过程,如下:

1)、JavaCompiler.enterTrees()方法调用Enter.main()方法;

根据上一步生成的抽象语法树列表完成填充符号表,返回填充了类中所有符号的抽象语法树列表;

2)、Enter.main()方法调用中本类的complete()方法;

complete()方法先调用Enter.classEnter()方法完成填充包符号、类符号以及导入信息等;

3)、接着complete()方法还会不断调用前面生成的每个类的类符号实例的plete()方法;

plete()方法会调用到plete(),以完成整个类的填充符号表;

4)、plete()中会添加类的默认构造函数(如果没有任何的);

还会调用 MemberEnter.finish()方法完成对类中字段和方法符号的填充;

等等(其实先处理注解信息)…

注意,EnterTrees()方法最终完成返回一个待处理列表(”todo” list),其实该列表还是抽象语法树列表,符号只是填充到上一步生成的抽象语法树列表中;可以从上面语法分析给出的JCMethodDecl类中看到有一个MethodSymbol类的成员变量;

3、编译测试

还用上面的JavacTest.java文件测试,其中getI()方法的符号如下(显示符号名称):

测试JavacTest.java文件填充符号表的前后,抽象语法树列表变化(红色)如下:

4、计算方法的特征签名

其实MethodSymbol方法符号中的MethodType类型的type成员就是其特征签名;

在.MemberEnter.visitMethodDef(JCMethodDecl tree)中填充方法符号的时候计算特征签名,如下: publicvoidvisitMethodDef(JCMethodDecl tree) { ...... MethodSymbol m = new MethodSymbol(0, tree.name, null, enclScope.owner); ...... // Compute the method type m.type = signature(m, tree.typarams, tree.params, tree.restype, tree.recvparam, tree.thrown, localEnv); ...... }

MethodType如下: publicstaticclassMethodTypeextendsTypeimplementsExecutableType{ public List argtypes; public Type restype; public List thrown; /** The type annotations on the method receiver. */public Type recvtype; publicMethodType(List argtypes, Type restype, List thrown, TypeSymbol methodClass){ super(methodClass); this.argtypes = argtypes; this.restype = restype; this.thrown = thrown; } ......

可以看到特征签名包含了返回值类型,其实方法特征签名在Java语言层面和JVM层面是不同的:

Java语言层面特征签名:

方法名、参数类型和参数顺序;

JVM层面特征签名:

方法名、参数类型、参数顺序和返回值类型;

这个在后面文章介绍Class文件格式再详细说明;

5、添加默认类实例构造函数、”this”类变量符号、”super”父类变量

这个阶段,编译器自动添加默认类实例构造函数、”this”类变量符号、”super”父类变量符号:

(a)、如果类中没有定义任何实例构造函数,编译器会自动添加默认的类实例构造函数;

在完成一个类的填充符号时调用: plete(Symbol sym){ ...... // Add default constructor if needed. if ((c.flags() & INTERFACE) == 0 && !TreeInfo.hasConstructors(tree.defs)) { ...... if (addConstructor) { MethodSymbol basedConstructor = nc != null ? (MethodSymbol)nc.constructor : null; JCTree constrDef = DefaultConstructor(make.at(tree.pos), c, basedConstructor, typarams, argtypes, thrown, ctorFlags, based); tree.defs = tree.defs.prepend(constrDef); } ...... } ...... }

测试JavacTest.java文件添加的实例构造函数如下:

可以看到添加的类实例构造名称为(),虽然树结构上名称还是表现为”JavacTest”;

还有添加的时候会判断当前类的类型如果不是Object类型,都会在构造函数里添加”super();”,表示调用父类的构造函数,如下:

(b)、添加”this”类变量

在类实例作用域添加”this”符号,表示当前类实例,如下:

(c)、”super”父类变量符号

接着,在类实例作用域添加”super”符号,表示类父,如下:

3、插入式注解处理器的注解处理过程

JDK1.5后,Java语言提供了对注解(Annotation)的支持,注解和Java代码一样,都是在运行期间发挥作用;

JDK1.6中提供一组插件式注解处理器的标准API,可以实现API自定义注解处理器,干涉编译器的行为;

注解处理器可以看作编译器的插件,在编译期间对注解进行处理,可以对语法树进行读取、修改、添加任意元素;但如果有注解处理器修改了语法树,编译器将返回解析及填充符号表的过程,重新处理,直到没有注解处理器修改为止,每一次重新处理循环称为一个Round。

如hibernate Validator Annotation Process:用于校验Hibernate标签。

1、源码分析

注解处理器的初始化过程在JavaCompiler.initProcessAnnotations()方法中完成;

执行过程则是JavaCompiler.processAnnotations()方法;

如果有多个注解处理器,在JavacProcessingEnvironment.doProcessing()继续处理;

2、注解处理器实现与运行

代码实现:继承抽象类javax.annotation.processing.AbstractProcess,并覆盖abstract方法:”process()”;

运行/测试:通过javac -processor参数附带编译时的注解处理器;

这里我们没有实现注解处理器,运行javac编译JavacTest.java不会处理语法树; 4、语义分析与字节码生成

上面我们获得了填充了符号表的抽象语法树列表;

它能表示程序的结构,但无法保证程序的符合逻辑。 4-1、语义分析

主要任务是对结构上正确的源程序进行上下文有关性质的审查(如类型审查);

语义分析过程分为标注检查、数据及控制流分析两个步骤; 4-1-1、标注检查

1、概念解理

标注检查步骤检查的内容包括变量使用前是否已被声明、变量与赋值的数据类型是否能匹配等;

还有比较重要的动作称为常量折叠;

如前面测试程序”int i;”改为”int i=1+2;”,会被折叠成字面量”3″,与”int i=3″一样,如图:

2、源码分析

主要由com.sun.p.Attr类和com.sun.p.Check类完成,调用关系如下图:

由JavaCompiler.attribute()入口分析整个类的语法树的标注;

到Attr.attribClassBody()分析类的主体部分,如进行所有定义的检查:

comp.Check类的实例在Attr.attribClassBody()分析中进行定义、类型等检查;

如”boolean k = 1″,最终是通过类型检查赋值数据”1″的类型”int”不是接收者”k”的类型”Boolean”的父类来确定错误,如下:

3、自动添加super():

方法检查时,如果发现(自己定义的)类实例构造函数没有显式调用super()或this(),会添加super()的父类构造函数调用,如下:

但是,前面说过如果没有自定定义任何构造函数,前面填充符号表时,就已经添加含有super()的默认构造函数了;

4、标注检查结果

标注检查中已经使用Env类实例作为类编译信息的存储形式,它包含了一些访问上下文环境。

还是前面的测试程序,标注检查前后变化(红色)如下:

4-1-2、数据及控制分析

1、概念解理

数据及控制分析是对程序上下方逻辑更进一步的验证;

如检查变量的初始化、方法每个执行分支是否都有返回值、是否所有的异常都被正确处理等;

注意这阶段并不会对变量赋值;

这个时期与类加载时的数据及控制分析的目的一致,但校验范围不同;

如final修饰的局部变量:

final修饰的局部变量是在这个编译阶段处理的;

有没有final修饰符,编译出来的Class文件都一样,在常量池没有CONSTANT_Fiedref_info称号引用;

即在运行期没有影响,参数不变性由编译器在编译期保障;返回搜狐,查看更多

责任编辑:

如果觉得《java 源码分析_Java 源代码编译成 Class 文件的过程分析》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。