aas 从计算机程序出现的第一天起,对效率的追逐就是程序员天生的坚定信仰,这个过程犹如一场没有终点、永不停歇的F1方程式竞赛,程序员是车手,技术平台则是在赛道上飞驰的赛车。
asdsadasdasdasdsadasdasdasdsadassdasdsasdsadsdasdasdsadasdasdsadasdsadassadasdas————《Java虚拟机规范》
"编译期"到底指什么?
aa
aas[注]:
aaasdas①、一般我们说的“前端”指的是由前端编译器完成的编译行为。
aa
aaasdas②、Java虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中(这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也同样能享受到编译器优化措施所带来的性能红利)。
aa
aaasdas③、相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖字节码或者Java虚拟机的底层改进来支持。
aa
aaasdas总之:Java中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高。
Javac编译器的相关知识:
aa
aasJavac编译器身就是一个由Java语言编写的程序。
das从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程:
das
dasdas①、准备过程:初始化插入式注解处理器。
das
dasdas②、解析与填充符号表过程(词法、语法分析;构造出抽象语法树;填充符号表:产生符号地址和符号信息)
das
dasdas③、插入式注解处理器的 注解处理过程
das
dasdas④、分析与字节码生成过程(标注检查(对语法的静态信息进行检查)、数据流及控制流分析(对程序动态运行过程进行检查)、解语法糖(将简化代码编写的语法糖还原为原有的形式)、字节码生成(将前面各个步骤所生成的信息转化成字节码))
das
dasdas上述3个处理过程里,执行 插入式注解时 又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号。
aas1.1 解析过程: 包括了经典程序编译原理中的词法分析和语法分析两个步骤:
aa
aaasdsas①、词法分析是将 源代码的字符流 转变为 标记集合 的过程。(字符流 ⏩ 标记集合)
aaasdasdsasas⒈单个字符是程序编写时的最小元素,但标记才是编译时的最小元素,键字、变量名、字面量、运算符都可以作为标记。
aaasdasdsasas⒉比如,“int a=b+2”这句代码中就包含了6个标记,分别是int、a、=、b、+、2。
aa
aaasdsas②、语法分析是 根据标记序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。 ( 构建抽象语法树)
aa
aas1.2 填充符号表过程: 完成了语法分析和词法分析之后,下一个阶段是对符号表进行填充的过程。
aa
aaasdsas①、符号表是由一组符号地址和符号信息构成的数据结构,读者可以把它类比想象成哈希表中键值对的存储形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表、栈结构符号表等各种形式)
aa
aaasdsas②、符号表中所登记的信息在编译的不同阶段都要被用到。譬如在语义分析的过程中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码,在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。(对于语义分析的验证)
aa
aas2 注解处理器:
aa
aaasdsas①、JDK 5之后,Java语言提供了对注解的支持,注解在设计上原本是与普通的Java代码一样,都只会在程序运行期间发挥作用的。
aa
aaasdsas②、但在JDK 6中又设计了一组被称为“插入式注解处理器”的标准API,可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。
aasadsas[注]:
aasasdsaasas⒈我们可以把插入式注解处理器看作是一组编译器的 插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环过程称为一个轮次。
aasdsasadsas⒉有了编译器注解处理的标准API后,程序员的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件中被访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够的创意,程序员能使用插入式注解处理器来实现许多原本只能在编码中由人工完成的事情。
aasdsasadsas⒊比如Java著名的编码效率工具Lombok,它可以通过注解来实现自动产生getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()方法,等等,帮助开发人员消除Java的冗长代码,这些都是依赖插入式注解处理器来实现的。
aa
aas3 语义分析与字节码生成: (依靠抽象语法树对程序逻辑进行验证)
aa
aaasdsas经过语法分析之后,编译器获得了程序代码的抽象语法树表示,虽然能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。
aaasdsas语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,等等。
aaasdsasJavac在编译过程中,语义分析过程可分为标注检查和数据及控制流分析两个步骤(当然还有其他),
aa
aaasdsas⒈标注检查:
aa
aaasasddsas检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。在标注检查中,还会顺便进行一个称为 常量折叠 的代码优化,这是Javac编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
aasasdadsas[注]:比如对于int a = 1 + 2
,在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但在经过常量折叠优化之后,它们将被折叠为"3"。因此,在代码里面定义“a=1+2”比起直接定义“a=3”来,并不会增加程序运行期哪怕仅仅一个处理器时钟周期的处理工作量。
aa
aaasdsas⒉数据及控制流分析
aa
aaasasddsas数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序 局部变量 在使用前是否有赋值、方法的每条路径是否都有 返回值、是否所有的 受查异常 都被正确处理了等问题;
aasasdadsas[注]:
aasasdadsasds①、编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上可以看作是一致的,但校验范围会有所区别,有一些校验项只有在编译期或运行期才能进行。
aasasdadsasds②、【局部变量是否用final修饰对于Class文件都是一样?】 局部变量在常量池中并没有CONSTANT_Fieldref_info
的符号引用,自然就不可能存储有访问标志(access_flags)的信息,甚至可能连变量名称都不一定会被保留下来(这取决于编译时的编译器的参数选项),自然在Class文件中就不可能知道一个局部变量是不是被声明为final了。因此,可以肯定地推断出把局部变量声明为final,对运行期是完全没有影响的,变量的不变性仅仅由Javac编译器在编译期间来保障。
aa
aaasdsas⒊解释语法糖
aa
aaasasddsas语法糖,也称糖衣语法,由英国计算机科学家Peter J.Landin发明的一种编程术语,指的是在计算机语言中添加的某种语法。这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言。通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
aasasdadsas[注]:
aasasdadsasds①、Java在现代编程语言之中已经属于“低糖语言”(这也是Java语言一直被质疑是否已经“落后”了的一个浮于表面的理由)。
aasasdadsasds②、Java中最常见的语法糖包括了前面提到过的 泛型、变长参数、自动装箱拆箱,等等。Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。
aa
aaasdsas⒋字节码生成
aa
aaasasddsas字节码生成是Javac编译过程的最后一个阶段。
aa
aaasasddsas字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。(在这个时期把实例构造器< init>()方法和类构造器< clinit>()方法就是在这个阶段被添加到语法树之中的)。
aasasdadsas[注]:
aasasdadsasds①、这里的实例构造器并不等同于默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、可访问性(public、protected、private或< package>)与当前类型一致的默认构造函数,这个工作在填充符号表阶段中就已经完成==。
aasasdadsasds②、< init>()和< clinit>()这两个构造器的产生实际上是一种代码收敛的过程,编译器会把语句块、调用父类的实例构造器(仅仅是实例构造器,< clinit>()方法中无须调用父类的()方法,Java虚拟机会自动保证父类构造器的正确执行,但在< clinit>()方法中经常会生成调用java.lang.Object的< init>()方法的代码)等操作收敛到< init>()和< clinit>()方法之中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动作由Gen::normalizeDefs()方法来实现。除了生成构造器以外,还有其他的一些代码替换工作用于优化程序某些逻辑的实现方式,如把字符串的加操作替换为StringBuffer或StringBuilder(取决于目标代码的版本是否大于或等于JDK 5)的append()操作,等等。
aaasasddsas③、完成了对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表到com.sun.tools.javac.jvm.ClassWriter
类手上,由这个类的writeClass()方法输出字节码,生成最终的Class文件,到此,整个编译过程宣告结束。