本文主要总结Java编译器的整体流程,给读者一个宏观的印象。了解Java编译器整体设计思想比了解《深入解析Java编译器》各章节的具体内容更重要。如果不从整体设计出发、思考各个步骤及其关系,修bug的时候,只能像个无针织毛衣头苍蝇一样到处乱撞,不利于问题解决,更不利于个人的发展。
相关链接编译器前端理论知识一个编译器前端主要分为:词法分析、语法分析、语义分析、中间代码生成等阶段。下面主要列举一些基本概念,方便读者回忆。具体算法还要读者自己去看编译原理相关的书籍。
词法分析(Lexer):输入是字符流,输出是token流。通过将正则表达式(RE)转换成 非确定的有限自动机(NFA),NFA再转换成确定的有限自动机(DFA),最后最小化DFA。根据最小化的DFA,可以很容易地编码实现词法分析器。一些词法分析器产生器可以根据用户提供的正则表达式直接生成词法生成器。
语法分析(Parser):输入是token流,输出是抽象语法树。语言的语法通常使用上下文无关文法表示,因为正则的表达能力有限。之后就是各种各样的分析方法:LL(0)、LL(1)、LL(K)、LR(0)、LR(1)、LR(K)、LALR(1)、SLR等等。注意:根据我的经验,死记这些分析方法是没用的,很容易就忘了,所以只需要记住你当前修改的编译器对应的分析方法及相关内容就行化痰的食物了,其他的内容,以后用到再回来看。一些语法分析器产生器可以根据用户提供的文法内容直接生成语法生成器。语法分析和词法分析两个步骤可能合在一起,使得编译器只遍历一次输入流。还有grammar这个英文宾夕法尼亚单词会翻译成“语法”、“文法”,所以“语法”、“文法”可能会在一些理论书或者本文中混用,读者一般可以把他们当成一样的内容。
语义分析(semantic analysis):输入是抽象语法树,输出是符号表和标注的语法树,同时可能生成属性(Attribute)、环境(Env)、上下文(Context)、作用域(Scope)之类的内容。一般的理论书都说提到:特设的语法制导翻译(Ad hoc syntax-directed translation)、属性文法(Attribute grammar)。这一部分其实是编译器前端最复杂的部分,因为工业语言的规范文档一般1000多页、10几到几十章的内容,一般只有一章是词法结构、入党一章是语法结构汇总,其他的大部分都是语义分析的内容。我们看书的时候可能觉得语法分析、特别是LR语法比较难,但是到了实际看代码的时候,估计会被语义分析的代码绕晕,而语法分析的代码却很容易被理解。
中间代码生成(code shape、translate):输入是标注的语法树及其相关内容,输出是中间代码。最基本的问题是中间表示(IR)的选取,可以有图表示(树可以说是特殊的图,读者把树分为单独一类也行)、线性表示。图(和树)IR最常见的就是抽象语法树了。而线性IR又有0、1、2、3地址码等分类。这些IR都有他们的优缺点。而我们作为初学者去修改开源编译器代码的时候,很多时候都是遵循原有的IR格式,很少有机会去重新设计IR。之后就是源代码到IR的对应关系和转换,这步由源语言和IR格式来决定。不同的源语言语法和不同的IR造成了这一步的内容多样性。这要求编译器开发者要充分了解其编译器的源语言和IR的具体内容。
Java编译器Java编译器有很多代码,我大概把他们分成两类:编译流程相关内容、编译流程无关内容。其中编译流程无关内容这里不详细介绍。
编译流程相关内容:
Parse:解析过程。输出是源程序字符流,输出是抽象语法树,顶层的语法树是JCCompilationUnit,直译就是编译单元。主要的操作类在com.sun.tools.javac.parser包中,主要的数据结构在包com.sun.tools.javac.tree中。包括词法分析器lexer和语法分析科林费斯器parser。
Init Module和Enter:构建符号表。输入是抽象语法树,树木希林输出是符号表(Symtab类的内容)、环境(Env类)和上下文(AttrContext类),其中Env里面含有对应的输入的语法树和AttrContext。一个由Env构成的队列,被抽象成类Todo(即Queue<Env>)。主要的操作类在com.sun.tools.javacp包中,主要的数据结构在包com.sun.tools.javac.code和包com.sun.tools.javac.tree中。
Annotation Processing:注解处理。构建符号表的时候,会把遇到的注解全部保存下来,给这一阶段使用。输入是抽象语法树和上个阶段保存的各个注解,输出是根据用户自定义的注解处理器决定的。注解处理器是用户自己写的代码,使编译器遇到某些注解的时候可以执行用户自定义的代码,我们平常开发一般用不到这个功能,想详细了解的读者可以自行看其他资料。既然遇到注解的时候执行的代码是用户自定义的,那如果用户使用注解处理器生成java源代码,编译器会怎样处理这些新的Java源代码呢?编译器会检测注解处理器是否生成新的Ja公务员备考va源程序,如果生成新的Java源程序,编译器会重新执行Parser、Init Module、Enter这三个阶段,对新的源程序进行处理,然后又重新进行Annotation Processing注解处理。这样循环下去,直到没有新的源程序生成。上文链接里面有一些图,比较形象地描述这个过程,读者可以自行参考。主要的操作类在com.sun.tools.javac.processing包中,主要的数据结构在包com.sun.tools.javac.code和com.sun.tools.javac.tree中。
Attr:属性标注。输入是上面几个阶段生成的内容“Todo对象”,也就是一个由Env组成的队列(即Queue<Env>),注意一个Env里面有一个上下文AttrContext和对应的语法树,AttrContext还有一个作用域Scope。输出还是由一个由Env组成的队列(即Queue<Env>),只不过里面的语法树多了类型Type和符号Symbol等信息,Env、AttrContext、Scope的很多字段都进行了初始化。这一阶段主要包括类型检测、命名解析、常量折叠、类型推导等内容,其中对函数参数、匿名类、lambda表达式、方法引用做了很多错综复杂的操作。很多bug都出现在这一阶段。主要的操作类在com.sun.tools.javacp包中,主要的数据结构在包com.sun.tools.javac.code和com.sun.tools.javac.tree中。
Flow:数据流分析。输入和输出都是上文说的由Env组成的队列(即Queue<Env>)。这一阶段主要对数据流的合法性进行多种分析,更新Env的情况很少,不像Attr,Attr几乎每一步都会设置类型和符号或者作用域,而Flow一般是分析完后,只根据分析的结果进行报错或者不报错。这些数据流分析包括活跃性分析、异常捕获分析、有限赋值分析、本地变量捕获分析等,而且这些分析种类是分开的,一般互不影响。相对于Attr的错综复杂,Flow对各个分析分开实现,真厨房装修效果图是太好理解了。主要的操作类在com.sun.tools.javacp包中,主要的数据结构在包com.sun.t德勤笔试ools.javac分词器.code和com.sun.tools.javac.tree中。
Desugar:解(de)语法糖(sugar)。输入是由Envtelent组成的队列,输出是由Env和类语法树的二元组(即Pair<Env, JCClassDecl>)组成的队列(即Queue<Pair<Env, JCClassDecl>>)。Desugar会执行范型擦除、模式转换、lambda转换、内部类转换、foreach语句转换等操作。JDK 8及之后的版本 所加的语言功能基本都可以在这里找到对应的转换操作。Desugar遍历语法树,在遇到这些“高级花斑癣”语法结构的时候,就把它转换成对应的“低级”语法结构。主要的操作类在com.sun.tools.javacp包中,主要的数据结构在包com.sun.tools.javac.code和com.sun.tool童虎s.javac.tree中。
Generate:中间代码生成。输入是desugar生成的由Env和类语法树的二元组(即Pair<Env, JCClassDecl>)组成的队列(即Queue<Pair<Env, JCClassDecl>>),输出就是我们平常看到的class文件了。这一步的关键是理解字节码和class文件结构,还有这些内容和Java语法结构的对应关系。JLS和JVMS对这些内容做了详细说明。我建议想对JDK、JVM进行开发的读者:根据遇到的bug去读JLS,比如遇到关于枚举的bug,就去读JLS关于类和枚举那一章。而对于梅尔维尔鲸JVMS,除了4.10 Verification of class Files之外,其他都应该完整看完,其中第7章The Java Virtual Machine Instruction Set可以粗看但是不能不看。第7章类似一个手册,但是不粗读一遍,很难理解中间代码生成步骤的内容。了解了Java语言和字节码及他们的对应关系,这个阶段的代码就很容易萨博93理解了。
上述这些阶段对应的实现类、相关类 在上文的链接中都有说明,这里不再重复。
编译流程无关的内容这里不详细说明,大概有如下这些:
参数处理(Option Handling)、文件管理(File Manager等)、编译器调用方式(Invocation)、错误报告(Reporting、Diagnostics)、插件处理(Plugins)等。
理论知识和Java编译器的对应关系词法分析:对应parse阶段里lexer执行的操作。
语法分析:对应parse阶段里parser执行的操作。
语义分析:对应Init Module、Enter、Attr、Flow等阶段,而Annotation Processing注解处理是java独有的功能,硬要归类的话,也是属于语义分析。
代码生成:对应Desugar、Generate阶段。
给本书的读者的建议当你满足下面的条件的时候,我才建议你看《深入解析Java编日本负利率译器 源码剖析与实例详解》这本书和Java编译器的代码:
熟练使用Java,并对Java语言的一些新特征有基本的了解。大概标准:去看类jdlpiler/com.sun.tools.javac.code.Source里面的内部枚举类Feature,里面是JDK7之后的所有语言功能。如果你对其中的一半内容看着眼熟,那就可以了。想学习编译原理并且以后想找编译器相关的工作。如果不想学编译原理或者不想找编译器相关工作,真的不建议读这些代码。因为编译器开发和应用开发的关注点不同,需要太多的时间和精力去学习和适应。如果不是真的感兴趣,还不如学Java的Web开发,学习成本不高而且工资也不低。如果不满足条赵洪祥件,我建议读者去看一些编译器整体设计、Java“高级”语法特征实现相关的文档和博客。对于编译器整体设计,可以看这篇文章:)。对于“高级”语法特征的实现,是上文“desuga同位素效应r解语法糖”阶段的内容的详细化,读者自行搜索吧。
还有一点,虽然《深入解析Java编译器 源码剖析与实例详解》书中讲解的代码是JDK 7的,但是我强烈建议读者直接看JDK主线的代码,尝试找bug和可以优化的地方,并且给OpenJDK提补丁。如果不提补丁,你永远不知道你对代码的理解是否正确,代码review过程可以验证你的想法和加深对代码的理解。如果不是为了修复bug和优化代码,看源代码意义不大,不如看相关设计和架构的文档和博客。如果不是看JDK主线的代码,你想给OpenJDK做贡献的时候,还是要再看一遍JDK主线的代码。有点绕。。
最近的感想我被提名为OpenJDK社区JDK项目的Committer了,这段时间的学习和对OpenJDK的贡献总算有了收获。这个提名给了我很大的鼓励,也让我深刻地明白了计算机是理论结合实践的一门学科。如果我1年前不选择给OpenJDK做贡献,估计我现在已经看完了龙书、虎书、鲸书、橡书,然后又忘记书中大部分内容……只看理论,不去实践,真的记不住那些理论知识,也不知道怎么去应用。
而我现在的状态是:看了橡书编译器前端相关的内容1-7章(2遍,第一遍看的英文版,是在给OpenJDK做贡献之前看的,在做贡献中途结合代码又看了一遍中文版)。看了虎书编译器前端相关的内容1-8章(1遍,中文版的)。而龙书是参考着看,遇到不懂的再去翻。合并到JDK主线的补丁有40个左右。
之后,我会逐渐把学习重心放在中端优化和后端代码生成上,主要目标是给OpenJDK的JIT编译器贡献代码。不过runtime是JIT的基础,所以估计会先深入runt桑黄ime。对于编译器前端,我应该也会定期看bug列表,争取1到2周解决一个bug。
厉害的前辈,可能会觉得差不多1年才学完编译器前端,太菜了。而不熟悉编译器的开发者或者学生,可能会觉得我成为了Committer,很厉害。不管怎样实蝇,我还是要把这些经历写出来。主要是为了让你们知道:“世界上还有这么一个(or一群)平常人,他不智力超群,他也不是低能儿。喜欢一样东西,他就去了解,去学习。一遍、两遍、很多遍,有一天,他终于弄懂了。”
共勉~
本文发布于:2023-06-02 15:07:52,感谢您对本站的认可!
本文链接:http://www.ranqi119.com/ge/85/192319.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |