概述
编译分为:
- 前端编译器:将Java源码编译为Class字节码(javac,ECJ)
- JIT:运行期将字节码转变为本地机器码(重排序就是此阶段发生的)(C1,C2,Graal)
- 提前编译器:直接将源码编译为与目标机器指令集相关的二进制代码的过程。(jaotc、GCJ、Excelsior JET)
这一章是讲第一部分的,这部分编译对于性能基本上没有优化,不过一些语言新特性是靠它实现的,而性能的优化主要是在运行期完成的。
Javac
javac是居然是Java写的!!!
编译字节码过程大致分为4个过程
- 准备过程:初始化插入式注解处理器
- 解析与填充符号表过程:1.词法,语法分析。2.填充符号表
- 处理插入式注解
- 分析与字节码生成:1.标注检查2.数据流及控制流分析3.解语法糖4.字节码生成。
解析与填充符号表
语义与语法分析
词法分析是将源码字符转化为Token(标记)的过程,Token是有具体含义的一个在编译期的最小元素(关键字、变量名、字面量、运算符都可以成为Token)。
语义分析是将标记序列构造成抽象语法树(想到了字典树),它的每一个节点都表示一个语法结构
在经过词义和语义分析后,编译期就不会对源码再进行操作了,而是在抽象语法树上进行。
填充符号表
一组地址信息和符号信息构成的数据结构,它所登记的信息在编译的不同阶段都会被用到。
注解处理器
注解原本只于运行期生效,JDK6以后JDK提供了一组被称为插入式注解处理器的API。
它们允许读取、修改、添加抽象语法树 中的任意元素(应用比如Lombok)
如果有插入式注解对语法树进行改动了,编译器将会回到解析及填充符号表的过程重新处理,每一次循环被称为一个轮次,直到没有修改了为止。
语义分析与字节码生成
经过语法分析后,编译器获得了程序代码的抽象化语法树表示,不过这这能表示语法上的正确,并不表示它合乎语义。这一阶段并有检查这个的功能。
标注检查
检查变量使用前是否被声明,类型是否匹配等。
这一阶段会进行一个被称为常量折叠的优化,比如a = 6+6 ,在语法树上就会被折叠为12,而不是6,+,6.
数据及控制流分析
它是对上下文逻辑的进一步分析,它可以检查出局部变量使用前是否赋值,方法的每条路径是否都有返回值,异常是否被正确处理等。
注:
由于局部变量并没有CONSTANT_Fielderf_info属性,所以不可能存储访问标志,所以Class文件中并不知道一个局部变量是否被声明为final。
解语法糖
JVM并不认识那些语法糖,需要编译期将它还原。
字节码生成
这个阶段会将前面生成的语法树符号表等信息转化为字节码指令写入磁盘,还有一些代码添加和转换工作。
< init>()方法和< clint>()方法就是于此阶段添加于语法树中的。(默认的空参实例构造器是在符号便期间就完成了),这两个方法会将一些初始化操作都收敛到其中(比如{}/static{}代码块,或者实例变量赋值/类变量赋值,调用父类的实例构造器等)。
还有一些优化程序某些逻辑的实现方式,如将字符串的+操作符替换为StringBuilder/StringBuffer的append()方法等。
编译过程就到此结束了。
语法糖
它大多是由前端编译器带来的(lambda不算)!!
泛型
Java的泛型被称为类型擦除式泛型,而C#被称为具现化式泛型。
Java在编译后的字节码文件中会被替换为原来的裸类型(就是不用泛型的类型),在运行期间,被标识为不同泛型类型的同种类型(比如List< String> 和List< Integer>)实际上是同种类型。无法创建泛型的对象,只能根据原类型使用。
Java的泛型实现具体来说,就是擦除为裸类型(在Code属性中进行擦除,元数据中仍然保存),然后在插入或者赋值时判断一下是否为泛型类型,或者能否重载。
这种实现方式的缺陷挺多:
- 比如将多个泛型赋值给一个原生类型,而类型间又不支持强转(比如int转Object),那就会无法添加进任何元素了(解决办法是不支持基本类型的转换),这就会带来频繁的拆箱装箱过程。
- 运行期无法获取泛型信息。
不过为了兼容以前版本也就选择了这种实现方式(大概)。
自动拆装箱与遍历循环
拆装箱被自动还原为.valueOf()方法或者.xxxValue()方法。(注:大小比较时慎用自动装拆箱,-128~127以内有缓存)
而遍历循环被还原为迭代器的实现
变长参数被还原为一个数组类型的参数。
条件编译
由于Java天生的编译环境,只需要用常量指定哪部分需要执行即可,永远不会执行的语句就不会被编译。