文章目录
- JIT
- 为什么说 Java 语言“编译与解释并存”?
- JIT原理
- JVM 架构简览
- JIT 编译流程
- JIT 编译器的实现
- 优化策略
- 方法内联
- 逃逸分析
JIT
在Java中,JIT(Just-In-Time)编译器是Java虚拟机(JVM)的一个重要组成部分,它负责将Java字节码转换成特定平台的机器码。这个过程是在Java程序运行时进行的,而非传统的编译过程中。Java的JIT编译器的目的是提高程序的执行效率,特别是对于长时间运行的Java应用来说,JIT编译可以显著提高性能。
这里是Java中代码从编译到运行的基本流程:
- 编译到字节码:首先,Java源代码被编译成Java字节码(.class文件)。这一步是静态的,发生在程序运行之前。
- 类加载:当Java程序运行时,Java虚拟机(JVM)会加载这些字节码文件。
- 字节码解释执行:最初,JVM通过解释器逐条执行字节码。这意味着JVM读取每条指令,然后执行对应的操作。这种方式简单但效率不高。
- JIT编译:当JVM识别出某些方法或代码块被频繁调用(即“热点代码”),它会使用JIT编译器将这些热点代码从字节码编译成本地机器码。由于机器码可以直接在硬件上执行,这样可以显著提高程序的执行速度。
- 优化:JIT编译器在编译过程中还会进行各种优化,比如内联、循环展开等,这些优化可以进一步提升代码的执行效率。
Java 程序从源代码到运行的过程如下图所示:
Java程序转变为机器代码的过程
我们需要格外注意的是 .class->机器码
这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。
JDK、JRE、JVM、JIT 这四者的关系如下图所示。
为什么说 Java 语言“编译与解释并存”?
我们可以将高级编程语言按照程序的执行方式分为两种:
- 编译型:编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
- 解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
编译型语言和解释型语言
根据维基百科介绍:
为什么说 Java 语言“编译与解释并存”?
这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
JIT原理
要深入理解Java中JIT(Just-In-Time)编译器的原理,我们需要从Java虚拟机(JVM)的架构和JIT编译的工作流程入手。
JVM 架构简览
JVM主要由以下几个部分组成:
- 类加载器(Class Loaders):负责加载类文件。
- 运行时数据区(Runtime Data Areas):存储各种运行时数据,包括堆(Heap)、栈(Stacks)、方法区(Method Area)、程序计数器(Program Counter Register)等。
- 执行引擎(Execution Engine):将字节码转换为机器码并执行。执行引擎中包括解释器(Interpreter)和JIT编译器。
JIT 编译流程
JIT编译器的工作流程大致如下:
- 解释执行:最初,执行引擎使用解释器逐条解释执行字节码。这种执行方式容易实现,但效率不高。
- 识别热点代码:JVM监控各个方法的执行情况,识别出被频繁执行的方法或代码块,即“热点代码”(Hot Spot Code)。
- 编译热点代码:JIT编译器将这些热点代码编译成相应平台的机器码。这个过程中,JIT编译器还会应用多种优化技术,比如方法内联、循环展开等,以提高代码的执行效率。
- 替换与执行:编译生成的机器码将替换原先的字节码执行路径。当再次执行到这些代码时,JVM将直接执行对应的机器码,从而提高执行效率。
JIT 编译器的实现
Java中的JIT编译器实现较为复杂,主要体现在优化策略和代码生成上。比如,OpenJDK中就包含了一个非常著名的JIT编译器:HotSpot VM。
优化策略
JIT编译器在将字节码转换成机器码的过程中,会应用一系列的优化策略来提高程序的执行效率。这些优化策略主要包括:
- 死码删除(Dead Code Elimination):编译器会识别并删除那些不会影响程序最终结果的代码段,例如永远不会被执行到的代码(dead code)。
- 循环优化(Loop Optimization):循环是程序中常见的热点,因此循环优化是JIT编译器的重点。这包括循环展开(减少循环次数来减小循环开销)和循环融合等技术。
- 方法内联(Method Inlining):将一个方法的内容直接替换到调用该方法的位置,以减少方法调用的开销。如果一个方法较小且频繁被调用,将其内联可以显著提高性能。
- 逃逸分析(Escape Analysis):分析对象的作用域和生命周期,如果对象不会“逃逸”出方法或线程,可能会被优化成栈上分配,减少垃圾收集的压力。
方法内联
方法内联它会把一些短小的方法体,直接纳入目标方法的作用范围之内,就像是直接在代码块中追加代码。这样,就少了一次方法调用,执行速度就能够得到提升,这就是方法内联的概念。
可以使用 -XX:-Inline
参数来禁用方法内联,如果想要更细粒度的控制,可以使用 CompileCommand 参数,例如:
ini代码解读
复制代码-XX:CompileCommand=exclude,java/lang/String.indexOf
在 JDK 的源码里,也有很多被 @ForceInline注解的方法,这些方法,会在执行的时候被强制进行内联;而被@DontInline注解的方法,则始终不会被内联。
JIT 编译之后的二进制代码,是放在 Code Cache 区域里的。这个区域的大小是固定的,而且一旦启动无法扩容。如果 Code Cache 满了,JVM 并不会报错,但会停止编译。所以编译执行就会退化为解释执行,性能就会降低。不仅如此,JIT 编译器会一直尝试去优化你的代码,造成 CPU 占用上升。
通过参数 -XX:ReservedCodeCacheSize
可以指定 Code Cache 区域的大小,如果你通过监控发现空间达到了上限,就要适当的增加它的大小。
逃逸分析
下面着重讲解一下逃逸分析,这个知识点在面试的时候经常会被问到。
有这样一个问题:我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?
答案是否定的,通过逃逸分析,JVM 能够分析出一个新的对象的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析现在是 JVM 的默认行为,可以通过参数-XX:-DoEscapeAnalysis
关掉它。
那什么样的对象算是逃逸的呢?可以看一下下面的两种典型情况。
如代码所示,对象被赋值给成员变量或者静态变量,可能被外部使用,变量就发生了逃逸。
public class EscapeAttr {Object attr;public void test() {attr = new Object();}
}
再看下面这段代码,对象通过 return 语句返回。由于程序并不能确定这个对象后续会不会被使用,外部的线程能够访问到这个结果,对象也发生了逃逸。
public class EscapeReturn {Object attr;public Object test() {Object obj = new Object();return obj;}
}
那逃逸分析有什么好处呢?
「1. 栈上分配」
如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力。
「2. 分离对象或标量替换」
但对象结构通常都比较复杂,如何将对象保存在栈上呢?
JIT 可以将对象打散,全部替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步分割的变量,比如 int、long 等基本类型)。也就是说,标量替换后的对象,全部变成了局部变量,可以方便地进行栈上分配,而无须改动其他的代码。
从上面的描述我们可以看到,并不是所有的对象或者数组,都会在堆上分配。由于JIT的存在,如果发现某些对象没有逃逸出方法,那么就有可能被优化成栈分配。
「3.同步消除」
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
注意这是针对 synchronized 来说的,JUC 中的 Lock 并不能被消除。
要开启同步消除,需要加上 -XX:+EliminateLocks
参数。由于这个参数依赖逃逸分析,所以同时要打开 -XX:+DoEscapeAnalysis
选项。
比如下面这段代码,JIT 判断对象锁只能被一个线程访问,就可以去掉这个同步的影响。
public class SyncEliminate {public void test() {synchronized (new Object()) {}}
}