java编译期是一个不确定的操作过程,可能是以下三种场景:
1.前段编译:将.java转变为.class文件的过程,如Javac。
2.即时编译(just in time):虚拟机后端运行期编译器将字节码转变为机器码的过程;
3.预编译AOT(Ahead of time):直接把*.class文件编译成本地机器码的过程。
Javac编译器对代码运行效率几乎没有任何优化措施,虚拟机设计团队把性能优化集中到后端的即使编译器中(jit),这样可以让不由Javac产生的字节码class文件也同样能享受到编译器优化带来的好处。相当多的新生java语法特性都是靠前段编译器javac的语法糖来实现的,而不是依赖虚拟机底层来支持。Java即时编译器在运行期的优化过程对于程序运行来说重要,而前段编译器优化过程对于程序编码来说关系更密切。
Javac前端编译器
Javac本身是一个由java语言编写的程序,javac将java文件编译成字节码class文件的步骤:
步骤一:解析与填充符号表过程
(1)词法、语法分析
词法分析:是将源代码的字符流转变为标记(Token)集合。单个字符是程序编写过程中的的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为标记,比如整型标志int由三个字符构成,但是它只是一个标记,不可拆分。
语法分析:是根据Token序列来构造抽象语法树的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。经过词法、语法分析这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。
(2)填充符号表
完成了语法分析和词法分析之后,下一步就是填充符号表的过程。
符号表是由一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到,在语义分析(后面的步骤)中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,对符号名进行地址分配时,符号表是地址分配的依据。
步骤二:插入式注解处理器的注解处理过程
插入式注解处理器是在API编译期间对注解进行处理,可以把它看成一组编译器插件,在插件里面可以读取、修改、添加抽象语法树中的任意元素,如果插件在处理注解期间对语法树进行修改,编译器将回到解析及填充符号表过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止。
步骤三:语义分析与字节码生成过程
(1)语义分析:语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是:对结构上正确的源程序进行上下文有关性的审查。语义分析过程分为标注检查和数据及控制流分析两个步骤;标注检查的内容包括诸如:变量使用前是否已被声明、变量和赋值之间的数据类型是否匹配等。数据及控制流分析是对程序上下文逻辑更进一步的验证,检查出诸如:程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
(2)解语法糖
语法糖能增加程序的可读性从而减少程序代码出错的机会。java语法糖是编译器实现的功能,不提供实质性的功能改进。Java中最常用的语法糖主要是泛型、变长参数、自动装箱拆箱等,虚拟机运行时不支持这些语法,他们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。条件编译时通过语法糖实现的。if条件为true,就不需要编译else代码。
(3)字节码生成
字节码生成是Javac编译过程的最后一个阶段。字节码生成阶段不仅仅是把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。 实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的。这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数,如果用户代码中没有提供任何构造函数,那编译器会自动添加一个没有参数、访问权限与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成了。
插入式注解处理实战
Javac编译器将java程序源码转换为字节码的时候,会对java程序源码做各方面的检查校验,这些校验主要是以程序写的对不对为出发点,但是对于程序写的好不好很少去关注,针对于写的好不好有如下辅助校验工具,CheckStyle、FIndBug等等,自己在AndroidStudio中用过findbug,这些代码校验工具有一些基于java源码进行校验,有一些通过扫描字节码来完成,在本节的实战中,通过使用注解处理API来编写一款拥有自己编码风格的校验工作:NameCheckProcesser;自己实现注解处理器,通过javac “-processor”参数来执行编译时需要附带的注解处理器,NameCheckProcesser具体内容请直接阅读《深入理解JVM》。