Java虚拟机(JVM)工作原理
虽然本教程的内容为 x86 处理器的原生汇编语言,但是了解其他机器架构如何工作也是有益的。JVM 是基于堆栈机器的首选示例。JVM 用堆栈实现数据传送、算术运算、比较和分支操作,而不是用寄存器来保存操作数(如同 x86 一样)。
JVM 执行的编译程序包含了 Java 字节码。每个 Java 源程序都必须编译为 Java 字节码(形式为 .class 文件)后才能执行。包含 Java 字节码的程序可以在任何安装了 Java 运行时软件的计算机系统上执行。
例如,一个 Java 源文件名为 Account.java,编译为文件 Account.class。这个类文件是该类中每个方法的字节码流。JVM 可能选择实时编译(just-in-time compilation)技术把类字节码编译为计算机的本机机器语言。
正在执行的 Java 方法有自己的堆栈帧存放局部变量、操作数栈、输入参数、返回地址和返回值。操作数区实际位于堆栈顶端,因此,压入这个区域的数值可以作为算术和逻辑运算的操作数,以及传递给类方法的参数。
在局部变量被算术运算指令或比较指令使用之前,它们必须被压入堆栈帧的操作数区域。通常把这个区域称为操作数栈(operand stack)。
Java 字节码中,每条指令包含 1 字节的操作码、零个或多个操作数。操作码可以用 Java 反汇编工具显示名字,如 iload、istore、imul 和 goto。每个堆栈项为 4 字节(32 位)。
下表给出了比较 op1 和 op2 之后压入堆栈的数值:
dcmp 指令比较双字,fcmp 指令比较浮点数。
goto 指令无条件分支到一个标号:
【示例 1】两个整数相加
下面的 Java 源代码行实现两个整数相加,并将和数保存在第三个变量中:
尽管字节码反汇编一般不包括注释,这里还是会将注释添加上去。虽然局部变量在运行时堆栈中有专门的保留区域,但是指令在执行算术运算和数据传送时还会使用另一个堆栈,即操作数栈。为了避免在这两个堆栈间产生混淆,将用索引值来指代变量位置,如 0、1、2 等。
现在来仔细分析刚才的字节码。开始的两条指令将一个常数值压入操作数栈,并把同一个值弹出到位置为 0 的局部变量:
接下来,为了实现加法,必须将两个操作数压入操作数栈。指令 iload_0 将变量 A 入栈,指令 iload_1 对变量 B 进行相同的操作:
这里并不关心这些例子的实际机器表示,因此上图中的运行时堆栈是向上生长的。每个堆栈示意图中的最大值即为栈顶。
指令 iadd 将栈顶的两个数相加,并把和数压入堆栈:
指令 istore_2 将栈顶内容弹出到位置为 2 的变量,其变量名为 sum:
【示例 2】两个 Double 类型数据相加
下面的 Java 代码片段实现两个 double 类型的变量相加,并将和数保存到 sum。它执行的操作与两个整数相加示例相同,因此这里主要关注的是整数处理与 double 处理的差异:
指令 dcmpl 将两个 double 数弹出堆栈进行比较。由于栈顶的数值(2.0)小于它下面的数值(3.0),因此整数 1 被压入堆栈。
虽然字节码的符号反汇编不如 x86 汇编语言简单,但是,编译器生成字节码也是相当容易的。每个操作都是原子的,这就意味着它只执行一个操作。
若 JVM 使用的是实时编译器,则 Java 字节码只要在执行前转换为本地机器语言即可。就这方面来说,Java 字节码与基于精简指令集(RISC)模型的机器语言有很多共同点。
Java 虚拟机
Java 虚拟机(JVM)是执行已编译 Java 字节码的软件。它是 Java 平台的重要组成部分,包括程序、规范、库和数据结构,让它们协同工作。Java 字节码是指编译好的 Java 程序中使用的机器语言的名字。JVM 执行的编译程序包含了 Java 字节码。每个 Java 源程序都必须编译为 Java 字节码(形式为 .class 文件)后才能执行。包含 Java 字节码的程序可以在任何安装了 Java 运行时软件的计算机系统上执行。
例如,一个 Java 源文件名为 Account.java,编译为文件 Account.class。这个类文件是该类中每个方法的字节码流。JVM 可能选择实时编译(just-in-time compilation)技术把类字节码编译为计算机的本机机器语言。
正在执行的 Java 方法有自己的堆栈帧存放局部变量、操作数栈、输入参数、返回地址和返回值。操作数区实际位于堆栈顶端,因此,压入这个区域的数值可以作为算术和逻辑运算的操作数,以及传递给类方法的参数。
在局部变量被算术运算指令或比较指令使用之前,它们必须被压入堆栈帧的操作数区域。通常把这个区域称为操作数栈(operand stack)。
Java 字节码中,每条指令包含 1 字节的操作码、零个或多个操作数。操作码可以用 Java 反汇编工具显示名字,如 iload、istore、imul 和 goto。每个堆栈项为 4 字节(32 位)。
查看反汇编字节码
Java 开发工具包(JDK)中的工具 javap.exe 可以显示 java.class 文件的字节码,这个操作被称为文件的反汇编。命令行语法如下所示:javap -c classname
比如,若类文件名为 Account.class,则相应的 javap 命令行为:javap -c Account
安装 Java 开发工具包后,可以在 \bin 文件夹下找到 javap.exe 工具。指令集
1) 基本数据类型
JVM 可以识别 7 种基本数据类型,如下表所示。和 x86 整数一样,所有有符号整数都是二进制补码形式。但它们是按照大端顺序存放的,即高位字节位于每个整数的起始地址(x86 的整数按小端顺序存放)。数据类型 | 所占字节 | 格式 | 数据类型 | 所占字节 | 格式 |
---|---|---|---|---|---|
char | 2 | Unicode 字符 | long | 8 | 有符号整数 |
byte | 1 | 有符号整数 | float | 4 | IEEE 单精度实数 |
short | 2 | 有符号整数 | double | 8 | IEEE 双精度实数 |
int | 4 | 有符号整数 |
2) 比较指令
比较指令从操作数栈的顶端弹出两个操作数,对它们进行比较,再把比较结果压入堆栈。现在假设操作数入栈顺序如下所示:下表给出了比较 op1 和 op2 之后压入堆栈的数值:
op1 和 op2 比较的结果 | 压入操作数栈的数值 |
---|---|
op1 > op2 | 1 |
op1 = op2 | 0 |
op1 < op2 | -1 |
dcmp 指令比较双字,fcmp 指令比较浮点数。
3) 分支指令
分支指令可以分为有条件分支和无条件分支。Java 字节码中无条件分支的例子是 goto 和 jsr。goto 指令无条件分支到一个标号:
goto label
jsr 指令调用用标号定义的子程序。其语法如下:jsr label
条件分支指令通常检测从操作数栈顶弹出的数值。根据该值,指令决定是否分支到给定标号。比如,ifle 指令就是当弹出数值小于等于 0 时跳转到标号。其语法如下:ifle label
同样,ifgt 指令就是当弹出数值大于等于 0 时跳转到标号。其语法如下:ifgt label
Java 反汇编示例
为了帮助理解 Java 字节码是如何工作的,本节将给出用 Java 编写的一些短代码例子。在这些例子中,请注意不同版本 Java 的字节码清单细节会存在些许差异。【示例 1】两个整数相加
下面的 Java 源代码行实现两个整数相加,并将和数保存在第三个变量中:
int A = 3; int B = 2; int sum = 0; sum = A + B;该 Java 代码的反汇编如下:
iconst_3 istore_0 iconst_2 istore_l iconst_0 istore_2 iload_0 iload_l iadd istore_2每个编号行表示一条 Java 字节码指令的字节偏移量。本例中,可以发现每条指令都只占一个字节,因为指令偏移量的编号是连续的。
尽管字节码反汇编一般不包括注释,这里还是会将注释添加上去。虽然局部变量在运行时堆栈中有专门的保留区域,但是指令在执行算术运算和数据传送时还会使用另一个堆栈,即操作数栈。为了避免在这两个堆栈间产生混淆,将用索引值来指代变量位置,如 0、1、2 等。
现在来仔细分析刚才的字节码。开始的两条指令将一个常数值压入操作数栈,并把同一个值弹出到位置为 0 的局部变量:
iconst_3 //常数(3)压入操作数栈
istore_0 //弹出到局部变量0
iconst_2 //常数(2)压入操作数栈
istore_1 //弹出到局部变量1
iconst_0 //常数(0)压入操作数栈
istore_2 //弹出到局部变量2
位置索引 | 变量名 |
---|---|
0 | A |
1 | B |
2 | sum |
接下来,为了实现加法,必须将两个操作数压入操作数栈。指令 iload_0 将变量 A 入栈,指令 iload_1 对变量 B 进行相同的操作:
iload_0 // (A 入栈)
iload_1 // (B 入栈)
这里并不关心这些例子的实际机器表示,因此上图中的运行时堆栈是向上生长的。每个堆栈示意图中的最大值即为栈顶。
指令 iadd 将栈顶的两个数相加,并把和数压入堆栈:
iadd
操作数栈现在包含的是 A、B 的和数:指令 istore_2 将栈顶内容弹出到位置为 2 的变量,其变量名为 sum:
istore_2
操作数栈现在为空。【示例 2】两个 Double 类型数据相加
下面的 Java 代码片段实现两个 double 类型的变量相加,并将和数保存到 sum。它执行的操作与两个整数相加示例相同,因此这里主要关注的是整数处理与 double 处理的差异:
double A = 3.1; double B = 2; double sum = A + B;本例的反汇编字节码如下所示,用 javap 实用程序可以在右边插入注释:
ldc2_w #20; // double 3.Id dstore_0 ldc2_w #22; // double 2.Od dstore_2 dload_0 dload_2 dadd dstore_4下面对这个代码进行分步讨论。偏移量为 0 的指令 ldc2_w 把一个浮点常数(3.1)从常数池压入操作数栈。ldc2 指令总是用两个字节作为常数池区域的索引:
ldc2_w #20; // double 3.ld
偏移量为 3 的 dstore 指令从堆栈弹出一个 double 数,送入位置为 0 的局部变量。该指令起始偏移量(3)反映出第一条指令占用的字节数(操作码加上两字节索引):dstore_0 //保存到 A
同样,接下来偏移量为 4 和 7 的两条指令对变量 B 进行初始化:
ldc2_w #22; // double 2.Od
dstore_2 // 保存到 B
dload_0
dload_2
dadd
最后,指令 dstore_4 把栈顶内容弹出到位置为 4 的局部变量:dstore_4
JVM 条件分支
了解 JVM 怎样处理条件分支是理解 Java 字节码的重要一环。比较操作总是从堆栈栈顶弹出两个数据,对它们进行比较后,再把结果数值入栈。条件分支指令常常跟在比较操作的后面,利用栈顶数值决定是否分支到目标标号。比如,下面的 Java 代码包含一个简单的 IF 语句,它将两个数值中的一个分配给一个布尔变量:double A = 3.0; boolean result = false; if( A > 2.0 ) result = false; else result = true;该 Java 代码对应的反汇编如下所示:
ldc2_w #26; // double 3.Od dstore_0 // 弹出到 A iconst_0 // false = 0 istore_2 //保存到 result dload_0 ldc2_w #22; // double 2.0d dcmpl ifle 19 //如果 A ≤ 2.0,转到 19 iconst_0 // false istore_2 // result = false goto 21 //跳过后面两条语句 iconst_l // true istore_2 // result = true开始的两条指令将 3.0 从常数池复制到运行时堆栈,再把它从堆栈弹岀到变量 A:
ldc2_w #26; // double 3.0d
dstore_0 // 弹出至A
iconst_0 // false = 0
istore_2 // 保存到 result
dload_0 //A 入栈
ldc2_w #22; // double 2.0d
指令 dcmpl 将两个 double 数弹出堆栈进行比较。由于栈顶的数值(2.0)小于它下面的数值(3.0),因此整数 1 被压入堆栈。
dcmpl
如果从堆栈弹出的数值小于等于 0,则指令 ifle 就分支到给定的偏移量:ifle 19 //如果 stack.pop() <= 0,转到 19
这里要回顾一下之前给出的 Java 源代码示例,若 A>2.0,其分配的值为 false:if( A > 2.0 ) result = false; else result = true;如果 A <= 2.0,Java 字节码就把 IF 语句转向偏移量为 19 的语句,为 result 分配数值 true。与此同时,如果不发生到偏移量 19 的分支,则由下面几条指令把 false 赋给 result:
iconst_0 // false
istore_2 // result = false
goto 21 //跳过后面两条指令
iconst_l // true
istore_2 // result = true
虽然字节码的符号反汇编不如 x86 汇编语言简单,但是,编译器生成字节码也是相当容易的。每个操作都是原子的,这就意味着它只执行一个操作。
若 JVM 使用的是实时编译器,则 Java 字节码只要在执行前转换为本地机器语言即可。就这方面来说,Java 字节码与基于精简指令集(RISC)模型的机器语言有很多共同点。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算