Java 语言的编译期大致可以分为三种:
不同的编译时期,为了提高代码的运行效率,JVM 会进行一定的编译优化。
早期编译主要是第一种编译,即把 .java 文件编译成 class 文件,这个过程的编译可以分为三个部分;
解析分为词法解析,和语法解析
将源代码中的字符流转变为标记集合,一个标记就是一个关键字,变量名,字面量,或者运算符. 比如
int a = 1 + b;
这部分的标记集合的元素就是:int , a , = ,1 , + ,b
将标记集合构造成抽象语法树的过程,抽象语法树就是程序代码语法结构的树形表示,每个节点都是一种语法的结构。
符号表,是一组符号地址和符号信息构成的表格.
注解处理器可以读取,修改,添加抽象语法树种的任何元素,如果修改了编译器就回到解析过程重新开始处理直到没有注解处理器对语法树进行修改,每次循环称为一个 Round,通过注解处理器就可以干涉编译器的行为.
分析主要是程序的语义分析,对抽象语法树中进行上下文性质的审查,如类型审查等。包括下面三种;
检查变量是否被声明,赋值之间的数据类型等,还有变量折叠 比如 int a = 1 + 2 ; 就会折叠成 int a = 3;
对程序上下文逻辑进行更进一步的验证,使用前是否赋值,方法是否有返回值等.
语法糖,也称糖衣语法.语法糖虽然不会提供实际性的功能改进,但是可以提高效率,提升语法的严谨性,减少编码出错的机会.虚拟机运行时不支持这些语法在编译阶段会还原简单的基础语法结构,并在相应的地方插入强制类型转换.Java 中的语法糖常见的有泛型,集合的遍历,变长参数,自动装箱和拆箱,条件编译等。
这一步就是把前面的步骤生成的信息转化为字节码写到磁盘中并进行少量的代码添加和转换工作,比如实力构造器 和类构造器
晚期优化主要是对一些热点代码进行优化。当虚拟机发现某个方法或代码块运行特别频繁时就会把这些代码认定为热点代码。在运行时候虚拟机会将这些代码编译成与本地平台相关的机器码,这个编译器叫即时编译器。而对于之前的一般的编译是通过解释器进行解释执行。两个各有区别,但也会相互配合工作。
当程序需要迅速启动和执行的时候,解释器可以省去编译时间,立即执行,解释执行可以节约内存。
在程序运行的时候,编译器可以把越来越多的代码编译成本地代码,提高执行效率。
解释器和编译器可以配合工作,可以从解释器转到即时编译器,
也可以从编译器逆优化到解释器。程序的执行一般先是解释器执行编译,在某些情况下解释器可启动即时编译器来进行一些优化编译,通常会为即时编译设置一个“逃生门”,这个逃生门的作用就是一旦一些优化不可执行编译的时候,即时编可以退回到解释器编译器,也叫逆优化。
即时编译器 (JIT 编译器),可以分为 Client 编译器和 Server 编译器,简称 C1 编译器和 C2 编译器。JVM 会根据不同的平台选取不同的编译器,以实现效率的提高。
为了让解释器和即时编译器能够实现效率最大化,一般将代码分为几个层次进行编译。
热点代码通常由两种;
判断是否为热点代码也有两种方式:
方法调用计数器,计数不是方法调用的绝对值,而是一段时间中的次数,
且这个次数没有超过阈值的时候会衰减。
回边计数器(循环体执行次数),计数的是执行的绝对次数。
提交编译请求后,解释方式会继续执行,而编译器的执行会在一个子线程中进行。
对于 C1,编译的过程可分为三个阶段:
对于这部分的优化实际上有很多方式,经典的有有如下几种:
如果一个表达式 A 已经计算过了,并且从先前的计算到现在的 A 中所有的变量的值都没有变化,那么这个 A 就可以成为子表达式。比如;
int a = b * c + (3 + b * c );
//优化
int a = A + (3 + A);
//优化
int a = 2 * A + 3;
当访问一个数组的时候,经常会对数组的边界进行检查。而可以采取的优化就是在数据流分析时确定对数组的访问不会超过 数组范围就不再每次进行数组越界检查。
就是将一些没用的代码剔除,或者对于没有必要的方法跳转,将目标方法中的代码 “复制”到发起调用的方法之中,避免真实的方法调用。因为 Java 中的多态性的存在因此,内联有时不能确定目标方法,对应的情况如下:
分析对象的动态作用域,当一个对象在方法中被定义后可能被外部方法所引用,例如传参,这称为方法逃逸,还有被其他线程访问的线程逃逸。
如果一个对象不会逃逸那么可以进行如下优化;