Java 代码运行流程
编译执行
直接将代码编译成 CPU 所能理解的代码格式,机器码。
比如下图的中间列,就是用 C 语言写的 Helloworld 程序的编译结果。可以看到,C 程序编译而成的机器码就是一个个的字节,它们是给机器读的。那么为了让开发人员也能够理解,我们可以用反汇编器将其转换成汇编代码(如下图的最右列所示)。
; 最左列是偏移;中间列是给机器读的机器码;最右列是给人读的汇编代码
0x00: 55 push rbp
0x01: 48 89 e5 mov rbp,rsp
0x04: 48 83 ec 10 sub rsp,0x10
0x08: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b]
; 加载 "Hello, World!\n"
0x0f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
0x16: b0 00 mov al,0x0
0x18: e8 0d 00 00 00 call 0x12
; 调用 printf 方法
0x1d: 31 c9 xor ecx,ecx
0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
0x22: 89 c8 mov eax,ecx
0x24: 48 83 c4 10 add rsp,0x10
0x28: 5d pop rbp
0x29: c3 ret
Java 为什么要在虚拟机里运行
高级程序语言的抽象程度高,无法直接在硬件上运行复杂的程序,所以在 Java 程序运行之前,需要对其进行一番转换。
为了实现这个转换的操作,设计一个面向 Java 语言特性的虚拟机,并通过编译器将 Java 程序转换成该虚拟机所能识别的指令序列,也称Java 字节码(Java 字节码指令的操作码(opcode)被固定为一个字节
)。
举例来说,下图的中间列,正是用 Java 写的 Helloworld 程序编译而成的字节码。可以看到,它与 C 版本的编译结果一样,都是由一个个字节组成的。并且,我们同样可以将其反汇编为人类可读的代码格式(如下图的最右列所示)。不同的是,Java 版本的编译结果相对精简一些。这是因为 Java 虚拟机相对于物理机而言,抽象程度更高。
# 最左列是偏移;中间列是给虚拟机读的机器码;最右列是给人读的代码
0x00: b2 00 02 getstatic java.lang.System.out
0x03: 12 03 ldc "Hello, World!"
0x05: b6 00 04 invokevirtual java.io.PrintStream.println
0x08: b1
Java 虚拟机实现的方式
Java 虚拟机可以由硬件实现 ,之所以操作软件现实的意义在于write once run anywhere
。
Java 虚拟机同时带来了一个托管环境(Managed Runtime)。它能够代替我们处理一些代码中冗长而且容易出错的部分。自动内存管理与垃圾回收,如数组越界、动态类型、安全权限等等的动态检测,托管环境可以使开发者可以更聚焦于业务。
Java 虚拟机运行 Java 字节码过程(HotSpot)
虚拟机视角
1. 执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中
2. 加载后的 Java 类会被存放于方法区(Method Area)中
3. 实际运行时,虚拟机会执行方法区内的代码。
硬件视角
Java 字节码无法直接执行,因此,Java 虚拟机需要将字节码翻译成机器码。
解释执行的优势在于无需等待编译,而编译执行的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
* 解释执行:即逐条将字节码翻译成机器码并执行
* 即时编译(Just-In-Time compilation,JIT):即将一个方法中包含的所有字节码编译成机器码后再执行。
Java 虚拟机的运行效率
HotSpot 采用了多种技术来提升启动性能以及峰值性能,刚刚提到的即时编译便是其中最重要的技术之一。
即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。
即时编译优于静态编译
理论上讲,即时编译后的 Java 程序的执行效率,是可能超过 C++ 程序的。这是因为与静态编译相比,即时编译拥有程序的运行时信息,并且能够根据这个信息做出相应的优化。
Java 虚拟机内置多个即时编译器应对不同场景
为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:C1、C2 和 Graal。Graal 是 Java 10 正式引入的实验性即时编译器.之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。
从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的编译线程中进行的。HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。
C1
C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。
C2
C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。