闲谈java中的程序编译与优化技术

java中的程序编译和优化技术同其他语言一样基本都发生在编译期。java的编译期可根据不同的编译器分为三个部分,一个是前端编译器,比如javac;它的工作就是把.java文件转化为.class文件。另一个是即时编译器,比如JIT编译器;它的工作是把.class文件中的某些热点字节码转化为本地机器码,提高程序运行速度。最后一个是静态提前编译器,比如AOT静态编译器。它跳过了.class文件的生成的过程,直接把.java文件转化为本地机器码。在某种意义上来说,这种方法已经放弃了java语言的平台无关性,无法“一次编译,到处运行”。前两个编译器分别代表了java语言的两个编译阶段,接下来我们来看看这两个阶段。

每一个编译器都由两个部分组成,一个是它本身就要完成的任务——编译代码,另一个则是为了优化程序而附带的功能——优化技术。前端编译器主要的优化工作针对于程序编码,它注重于方便程序员的开发和增强代码的可读性。而即时编译器的优化工作才是真正意义上的优化,它针对程序运行,注重于提高程序的运行效率。至于为何要把所有的程序运行优化技术都放在即时编译器中,一个很重要的原因就是java语言的平台无关性更确切地说应该是字节码(.class文件)的平台无关性。而能够产生.class文件的语言远不止java,包括Ruby等都可以。为了让这些语言也能够享受优化技术,研发人员就把提高程序运行效率的优化措施放到了即时编译器中。

前端编译器,它的编译过程主要由三个部分组成:解析和填充符号表、插入式注解处理器的注解处理过程以及语义分析和字节码生成过程。首先我们先来看一下解析和填充符号表的过程,它包括语法分析、词法分析和填充符号表三个部分。语法分析主要是将源代码的字符流变成标记(Token)集合。词法分析则是根据语法分析构建抽象语法树的过程。最后的符号表则是把符号引用的符号地址一一对应并保存起来。符号表是给符号引用分配地址的依据。插入式注解处理器可以读取、修改和添加抽象语法树的所有元素。如果抽象语法树的内容被修改了,那么编译要重新回到解析和填充符号表的阶段,直到所有的插入式注解处理器不再对抽象语法树进行修改为止。最后一个过程是语义分析和字节码生成过程。它包括标注检查、数据及控制流分析、解语法糖以及字节码生成四个阶段。

前端编译器的编码优化技术主要是通过java中的语法糖来实现的。java语法糖主要有:泛型和类型擦除、自动装箱拆箱、遍历循环和条件编译。其中java语言中的泛型属于伪泛型,它是基于类型擦除的方法实现的。也就是在经过javac编译后,泛型就会被还原成相应的具体的数据类型。而基于类型膨胀技术实现的泛型称为真实泛型。真实泛型在系统运行期生成,有自己的虚方法表和数据类型。也就是在java中List<Integer>和List<String>在编译后都会变成一样的原生类型List<E>。

即时编译器,它的编译过程主要包括一下几个部分。它发生作用的时间是在程序运行过程中,程序的启动是通过字节码解释器进行的。它的编译对象是被多次调用的方法以及被多次执行的循环体。即时编译器采用了分层编译的思想,包括第0层编译,字节码执行,可触发第1层编译;第1层编译,也成为C1编译,会把热点代码转化为本地机器码,进行简单可靠的优化;第2层编译,除了把热点代码转化为本地机器码之后,还会进行其他编译时间较长的优化措施和激进优化。当某一段代码成为热点代码时,就会触发即时编译,我们常通过热点探测技术来检测一段代码是否是热点代码。热点探测主要有两种,一种是基于采样的热点探测,它通过周期性地检查栈顶,如果某种方法常常出现在栈顶,就认为它是热点方法。另一种是基于计数器的热点探测。它需要为每个方法建立并维护计数器,当计数器的值超过当前阈值时,这个方法就会变成热点代码。JVM首先会检查当前方法是否存在本地机器码,如果有就直接执行本地机器码。如果没有,就对其进行即时编译。JVM会把即时编译的过程放到后台执行,当前程序先用热点方法的字节码继续运行。等到即时编译完成,再去用本地机器码。即时编译根据不同的运行环境可分为Server Compiler和Client Compiler。其中Client Compiler采取的优化程度主要是C1级别的优化,它只会进行局部性的优化,耗时短。而Server Compiler采取的优化成都主要是C2级别的优化,它会进行几乎所有经典的优化措施。

即时编译器的优化措施有很多,包括公共子表达式消除、数组边界检查消除、方法内联和逃逸分析。数组边界检查消除属于一种比较激进的优化措施,在数组溢出情况较少的时候能够提高不少的效率。但是如果数组溢出情况过多,反而会降低程序运行效率。方法内联除了能够消除方法调用的成本,更重要的是它能够为其它优化措施提供一个良好的代码基础。最后的逃逸分析是一种优化思路,不是具体的优化措施。代码逃逸主要主要分析对象的动态作用域。分为两种,一种是方法逃逸,也就是某个对象会被其他方法调用到,另一种是线程逃逸,也就是某个对象会被其他线程调用到。基于逃逸分析的优化思想,有以下三种优化措施。一个是栈上分配对象。如果某个对象不会被除当前方法以外的其他方法调用到,那么我们就可以直接把当前对象分配到栈上。那么这个对象就会随着当前方法的结束而自动销毁,如此一来可以减轻GC收集器不少的工作量。另一个是同步消除,如果某个变量不会被除当前线程以外的线程访问到,那么我们对这个变量所做的同步措施就没有必要了,可以把这些同步措施消除掉。最后一个是标量替换。如果一个对象不会被外部访问,并且这个对象可以被拆散的话,我们就可以不创建这个对象,转而把这个对象中被当前方法访问到的属性用标量表示,并且分配到当前方法的栈上。由于逃逸技术在当前还不是很成熟,因此在很多虚拟机中都是默认不开启。

 

该博文是本人阅读完《深入理解Java虚拟机》后做的一个知识点整合,更注重知识的关联性和完整性,因此不像其他博客一样有大小标题。没有JVM基础的建议先去看我的另外两篇博客《早期(编译期)优化(笔记)》《晚期(运行期)优化(笔记)》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章