Java 存在三类编译过程:
这三类编译过程分别对应下面三类编译器:
三类编译过程做的事情:
javac 编译器是一个由 Java 语言编写的程序。
在 jdk6 之前,javac 不属于 Java SE API 的一部分;jdk6 之后 javac 源码位于/langtools/src/share/classes/com/sun/tools/javac 中;jdk9 之后 javac 源码位于/src/jdk.compiler/share/classes/com/sun/tools/javac 中。
javac 的编译过程分为 1 个准备过程和 3 个处理过程:
(1)准备:初始化插入时注解处理器;
(2)解析与填充符号表过程,包括词法、语法分析和填充符号表;
(3)插入式注解处理器的注解处理过程;
(4)语义分析与字节码生成过程,包括标注检查、数据流及控制流分析、解语法糖、字节码生成。
()
和类构造器()
方法到语法树中)和转换工作(比如把字符串的加操作替换为 StringBuffer 或 StringBuilder 的 append 操作)。Java 程序在经过 javac 编译成为字节码之后,解释器一边将字节码解释为机器语言一边执行。为了提高代码的执行效率,对于执行次数比较频繁的代码块,虚拟机会把这些热点代码编译为机器码,当再次执行到这些热点代码的时候,就不需要解释器进行解释而可以直接执行了。
目前主流的 Java 虚拟机都采取的是这种解释器和即时编译器并存的运行架构。
解释器的优点:当程序启动的时候,可以立即运行。
即时编译器的优点:当程序启动后,随着时间的推移,越来越多的热点代码被编译为机器码,执行效率更高。
HotSpot 虚拟机中有两个即时编译器,分别为客户端编译器( Client Compiler)和服务端编译器(Server Compiler),又分别称为 C1 编译器和 C2 编译器。
在分层编译的工作模式出现之前,虚拟机通常采用解释器与一个编译器搭配的方式工作,这种模式称为“混合模式”,与之对应的有“解释模式”(只使用解释器)和“编译模式”(只使用编译器)。
为了在程序启动响应速度和运行效率之间达到平衡,虚拟机在编译子系统中加入了分层编译的功能,并划分了不同的编译层次:
使用分层编译后,解释器、客户端编译器和服务端编译器可以协同工作。用客户端编译器获取更快的编译速度,用服务端编译器获取更好的编译质量,而解释执行的时候也无需承担收集性能监控信息的任务。
即时编译器编译的目标是“热点代码”,热点代码主要有两类:
对于上述两类热点代码,编译器进行编译的目标都是整个方法体,而不是单独的循环体。
虚拟机采取“热点探测”来决定某段代码是不是“热点代码”,需不需要触发即时编译。主流的热点探测方法有两种:
HotSpot 虚拟机采取第二种基于计数器的热点探测方法,并且为每个方法准备了两类计数器:方法调用计数器和回边计数器。
当探测到存在热点方法时,执行线程会向虚拟机提交编译请求,编译动作默认在后台的编译线程中进行。在编译完成前,热点方法仍然按照解释方法继续执行。
对于客户端编译器,编译过程相对简单快速,更加关注局部性的优化。而服务端编译器更加关注全局的高复杂度的优化,会执行大部分经典的优化动作,比如无用代码消除、循环展开、消除公共子表达式等。
提前编译的发展方向有两条路:
方向一是为了解决即时编译的最大弱点:即时编译要占用程序运行时间和运算资源。
方向二的本质是给即时编译器做缓存加速,改善 Java 程序的启动时间,以及需要一段时间预热后才能到达最高性能的问题。
即时编译相比提前编译的优势:
编译器的目标是实现将程序代码翻译为本地机器码的工作,但是其难点在于翻译过程中的代码优化,是否能输出经过优化的高质量的代码。下面介绍几种最经典的优化方法:
最重要的优化技术,除了消除方法调用的成本(建立栈帧)外,还是其他优化方法的基础。
方法逃逸:方法中定义的对象可以作为调用参数传递到其他方法中。
线程逃逸:线程中定义的对象赋值给可以在其他线程中访问的实例变量,从而可以被其他线程所访问。
根据对象的动态作用域,逃逸程度可以分为:不逃逸、方法逃逸、线程逃逸。对于不同逃逸程度的对象,可以采取不同的优化:
(1)栈上分配:对于不逃逸和方法逃逸的对象可以在栈上分配内存
(2)同步消除:对于不会出现线程逃逸的变量,可以消除该变量的同步措施
如果一个表达式之前已经被计算过了,并且从先前的计算到现在表达式中所有变量的值都没有发生变化,那么该表达式就称为公共子表达式。对于这种表达式,直接使用前面的计算结果即可。
对于虚拟机子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作。如果在编译期可以确定数组表示下标的循环变量不会超过数组范围,那么在循环中就可以把整个数组的上下界检查消除掉。