编译期优化
解析和填充符号表
1.词法、 语法分析
词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、 变量名、 字面量、 运算符都可以成为标记。Token不可再拆分。
语法分析是根据Token序列构造抽象语法树的过程,抽象语法树(Abstract SyntaxTree,AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、 类型、 修饰符、 运算符、 接口、 返回值甚至代码注释等都可以是一个语法结构。
2.填充符号表
符号表(Symbol Table)是由一组符号地址和符号信息构成的表格,读者可以把它想象成哈希表中K-V值对的形式(实际上符号表不一定是哈希表实现,可以是有序符号表、 树状符号表、 栈结构符号表等)。
符号表中所登记的信息在编译的不同阶段都要用到。 在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。 在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
注解处理
在JDK 1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、 修改、 添加抽象语法树中的任意元素。 如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round。
语义分析与字节码生成
语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查(是否合乎语义逻辑必须限定在具体的语言与具体的上下文环境之中才有意义)。
1.标注检查
标注检查步骤检查的内容包括诸如变量使用前是否已被声明、 变量与赋值之间的数据类型是否能够匹配等。 在标注检查步骤中,还有一个重要的动作称为常量折叠。
2.数据及控制流分析
数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、 方法的每条路径是否都有返回值、 是否所有的受查异常都被正确处理了等问题(将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障)。
3.解语法糖
语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”。
泛型与类型擦除
本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。 这种参数类型可以用在类、 接口和方法的创建中,分别称为泛型类、 泛型接口和泛型方法。
在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。
C#里面泛型无论在程序源码中、 编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
自动装箱、拆箱、循环遍历
自动装箱、 拆箱在编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()与Integer.intValue()方法,而遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
条件编译
C、 C++中使用预处理器指示符(#ifdef)来完成条件编译。 C、 C++的预处理器最初的任务是解决编译时的代码依赖关系(如非常常用的#include预处理命令)
在Java语言之中并没有使用预处理器,因为Java语言天然的编译方式(编译器并非一个个地编译Java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间能够互相提供符号信息)无须使用预处理器。
条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,因此它只能实现语句基本块(Block)级别的条件编译,而没有办法实现根据条件调整整个Java类的结构。
4.字节码生成
把前面各个步骤所生成的信息(语法树、 符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作(如把字符串的加操作替换为StringBuffer或StringBuilder)。
运行期优化
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。
当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode),这时编译器完全不介入工作,全部代码都使用解释方式执行。
使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode),这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。 如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。 如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。 当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。 另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
Client Compiler
Server Compiler
几乎能达到GNU C++编译器使用-O2参数时的优化强度,它会执行所有经典的优化动作,如无用代码消除(Dead Code Elimination)、 循环展开(Loop Unrolling)、 循环表达式外提(Loop Expression Hoisting)、 消除公共子表达式(Common Subexpression Elimination)、 常量传播(Constant Propagation)、 基本块重排序(Basic Block Reordering)等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、 空值检查消除(Null Check Elimination)
- -XX:+PrintCompilation要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来
- -XX:+PrintInlining要求虚拟机输出方法内联信息
- -XX:+PrintAssembly参数要求虚拟机打印编译方法的汇编代码
- -XX:+PrintOptoAssembly(用于Server VM)或-XX:+PrintLIR(用于Client VM)来输出比较接近最终结果的中间代码表示。
常用编译优化技术
方法内联(Method Inlining)一是去除方法调用的成本(如建立栈帧等),二是为其他优化建立良好的基础。
冗余访问消除(Redundant Loads Elimination)
复写传播(Copy Propagation)
无用代码消除(Dead Code Elimination)
公共子表达式消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。 对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。
数组边界检查消除(Array Bounds Checking Elimination)
逃逸分析
如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
- -XX:+DoEscapeAnalysis手动开启逃逸分析
- -XX:+PrintEscapeAnalysis查看分析结果
- -XX:+EliminateAllocations开启标量替换
- -XX:+EliminateLocks来开启同步消除
- -XX:+PrintEliminateAllocations查看标量