Java-底层原理-编译原理
Java-底层原理-javac源码笔记
Java-底层原理-类加载机制
Java-底层原理-clinit和init
本文大量内容系转载自以下文章,并参考其他文档资料加入了一些内容:
我们可以通过javac命令将Java程序的源代码编译成Java字节码,即我们常说的class文件。这是我们通常意义上理解的编译。但是,字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java虚拟机做的,这个过程也叫编译。是更深层次的编译。在编译原理中,把源代码翻译成机器指令,一般要经过以下几个重要步骤:
根据完成任务不同,可以将编译器的组成部分划分为前端(Front End)与后端(Back End):
如下图所示,编译器可以分为:前端编译器、JIT 编译器和AOT编译器。下面我们逐个讲解。
对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件。JDK 的安装目录里有 javac
工具,就是它将 Java 代码翻译成字节码。相对于后面要讲的其他编译器,因为Javac处于编译的前期,因此又被成为前端编译器。
通过 javac 编译器,我们可以很方便地将 java 源文件翻译成字节码文件。就拿我们最熟悉的 Hello World 作为例子:
public class Demo{
public static void main(String args[]){
System.out.println("Hello World!");
}
}
我们使用 javac 命令编译上面这个类,便会生成一个 Demo.class 文件:
javac Demo.java
我们使用纯文本编辑器打开 Demo.class 文件,我们会发现是一连串的 16 进制二进制流。
运行 javac 命令的过程,其实就是 javac 编译器解析 Java 源代码,并生成字节码文件的过程,可以分为下面四个阶段:
clinit
和init
(不包括已在填充符号表时已执行的默认构造方法)在这时被添加到AST中)和转换(如将String的加转为StringBuilder.append
)少量代码。我们一般称 javac 编译器为前端编译器,因为其发生在整个编译的前期。常见的前端编译器有 Sun 的 javac,Eclipse JDT 的增量式编译器(ECJ)。
JIT 编译器(Just-In-Time Compiler)
当源代码转化为字节码之后,要运行程序有两种选择:
Code Cache
里(HotSpot在启动时,会为所有字节码创建在目标平台上运行的解释运行的机器码,并存放在CodeCache
中,在解释执行字节码的过程中,就会从CodeCache
中取出这些本地机器码并执行。),且之后无需重复解释。且在此过程中,会有大量优化策略!这两种方式的区别在于,前者启动速度快但运行速度慢(指令较多、基于内存是瓶颈速度慢于寄存器),而后者启动速度慢但运行速度快。因为解释器不需要像 JIT 编译器一样,将所有字节码都转化为机器码,自然就省去了优化的时间。而当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。且在JIT编译过程中,会有大量优化策略!
所以在实际情况中,为了运行速度以及效率,我们通常采用解释器和JIT相结合的方式(即混合模式)进行 Java 代码的编译执行。
在 HotSpot 虚拟机内置了两个即时编译器,分别称为 Client Compiler
和Server Compiler
。这两种不同的编译器衍生出两种不同的编译模式,我们分别称之为:C1 编译模式
,C2 编译模式
。
注意:现在许多人习惯上将 Client Compiler 称为 C1 编译器,将 Server Compiler 称为 C2 编译器,但在 Oracle 官方文档中将其描述为 compiler mode(编译模式)。所以说 C1 编译器、C2 编译器只是我们自己的习惯性称呼,并不是官方的说法。这点需要特别注意。
前面提到的会被JIT编译的热点代码
有两类:
目前主要的热点代码识别方式是热点探测(Hot Spot Detection),有以下两种:
HotSpot使用基于计数器的热点探测方法,为每个方法准备两个计数器。他们都会先查看是否存在已编译版本,如果有就优先执行已编译的本地代码。否则计数器加一,然后判断两个计数器之和超过阈值就触发JIT编译,否则以解释方式继续执行:
方法计数器。记录方法被调用次数。
回边计数器。是记录方法中的for或者while的运行次数的计数器。
关于OSR栈上替换
在回边计数器中,编译动作由循环体触发,编译器会以整个方法作为编译对象,也就是说会在方法执行过程中进行编译。那么就会发生方法栈帧还在栈内,方法就被替换了,即所谓OSR
栈上替换。
在触发编译时,执行引擎不会等待编译完成在执行,而是以解释执行方式继续执行字节码。直到编译完成,将方法的调用入口地址直接替换为新的编译后的代码地址。以后调用就可以都用已由JIT编译为机器代码的版本。
JIT除了具有缓存的功能外,还会对代码做各种优化。典型的有:
逃逸分析、 公共子表达式消除、方法内联、 数组边界检查消除、锁消除、锁粗化等
AOT 编译器的基本思想是:在程序执行前将源码直接生成 Java 方法的本地代码,以便在程序运行时直接使用本地代码。
但是 Java 语言本身的动态特性带来了额外的复杂性,影响了 Java 程序静态编译代码的质量。例如 Java 语言中的动态类加载,因为 AOT 是在程序运行前编译的,所以无法获知这一信息,所以会导致一些问题的产生。类似的问题还有很多,这里就不一一举例了。
总的来说,AOT 编译器从编译质量上来看,肯定比不上 JIT 编译器。其存在的目的在于避免 JIT 编译器的运行时性能消耗或内存消耗,或者避免解释程序的早期性能开销。
在运行速度上来说,AOT 编译器编译出来的代码比 JIT 编译出来的慢,但是比解释执行的快。而编译时间上,AOT 也是一个中等的速度。所以说,AOT 编译器的存在是 JVM 牺牲质量换取性能的一种策略。就如 JVM 其运行模式中选择 Mixed 混合模式一样,使用 C1 编译模式只进行简单的优化,而 C2 编译模式则进行较为激进的优化。充分利用两种模式的优点,从而达到最优的运行效率。
使用解释器实现的编程语言实现里,通常:
- 至少会在解释执行前做完语法分析,然后通过树解释器来实现解释执行;
- 兼顾易于实现、跨平台、执行效率这几点,会选择使用字节码解释器实现解释执行
在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。
前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。
JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。
而 AOT 编译器则能将源代码直接编译为本地机器码。
这三种编译器的编译速度和编译质量如下:
编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
编译质量上,JIT 编译器 > AOT 编译器 > 解释执行。
而在 JVM 中,通过这几种不同方式的配合,使得 JVM 的编译质量和运行速度达到最优的状态。
《深入理解Java虚拟机》
对java平台的理解、java是解释执行吗?
Java为什么解释执行时不直接解释源码?
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩
HotSpot模板解释器目标代码生成过程源码分析
How exactly does the Java interpreter or any interpreter work?