- 硬件依赖性:基于寄存器的指令集直接依赖硬件寄存器,因此在不同的硬件平台上可能存在差异,可移植性较差。
- 编译器复杂性:基于寄存器的指令集需要考虑寄存器分配等复杂问题,编译器的实现相对较复杂。
基于栈的解释器执行过程
接下看我们具体看一个实际的代码示例:
代码语言:javascript复制public class Test {
public static void main(String[] args) {
int a = 114;
int b = 514;
int c = a b;
}
}
将上述代码保存为 Test.java
然后对其进行编译和反编译:
javac Test.java
javap -v Test.class
可以看到输出了如下内容:
代码语言:javascript复制Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/Test.class
Last modified 2023年9月6日; size 276 bytes
SHA-256 checksum 4064a19d96fe4d72c9d780ef819e1e937b120c31b37482e0b74c70e37c2a5601
Compiled from "Test.java"
public class Test
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Test
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Test
#8 = Utf8 Test
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 114
2: istore_1
3: sipush 514
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7
line 6: 11
}
SourceFile: "Test.java"
我们专注于下列信息:
代码语言:javascript复制public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 114
2: istore_1
3: sipush 514
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: return
其中:
public static void main(java.lang.String[])
:表示这是一个公共的静态方法,方法名为main
,它接受一个java.lang.String
类型的数组作为参数。descriptor: ([Ljava/lang/String;)V
:说明了方法的描述符,其中([Ljava/lang/String;)
表示参数类型为java.lang.String
类型的数组,V
表示方法的返回类型为void
。flags: (0x0009) ACC_PUBLIC, ACC_STATIC
:这是方法的标志,其中ACC_PUBLIC
表示该方法是公共的,ACC_STATIC
表示该方法是静态的。
我们针对其中的 main
入口代码 Code
展示解释器的执行过程,其中:
stack=2, locals=4, args_size=1
提示我们这段代码需要深度为 2 的操作数栈、 4 个变量槽的局部变量空间和 1 个方法参数。
根据给定的字节码指令,我们可以模拟执行程序并跟踪操作数栈、局部变量表和程序计数器的动态变化过程。
首先,我们创建一个操作数栈(operand stack)和一个局部变量表(local variable table),并初始化程序计数器(program counter)为0。
代码语言:javascript复制执行:0: bipush 114
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this(索引起始), null, null, null]
程序计数器状态:0
代码语言:javascript复制执行:2: istore_1
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:2
代码语言:javascript复制执行:3: sipush 514
操作数栈状态:[514(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:3
代码语言:javascript复制执行:6: istore_2
操作数栈状态:[nul(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:6
代码语言:javascript复制执行:7: iload_1
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:7
代码语言:javascript复制执行:8: iload_2
操作数栈状态:[114(栈顶), 514]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:8
代码语言:javascript复制执行:9: iadd
操作数栈状态:[628(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:9
代码语言:javascript复制执行:10: istore_3
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, 514, 628]
程序计数器状态:10
代码语言:javascript复制执行:11: return
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, 514, 628]
程序计数器状态:11
上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做出一系列优化来提高性能,实际的运作过程并不会完全符合概念模型的描述。
更确切地说,实际情况会和上面描述的概念模型差距非常大,差距产生的根本原因是虚拟机中解析器和即时编译器都会对输入的字节码进行优化,即使解释器中也不是按照字节码指令去逐条执行的。
关于编译器优化的细节,我们会在以后的系列文章中提到。
方法调用指令
指令概述
针对调用不同类型的方法,字节码指令集里设计了不同的指令:
invokestatic
:用于调用静态方法。可以在类加载时将符号引用解析为直接引用。invokespecial
:用于调用实例构造器<init>()
方法、私有方法和父类中的方法。也可以在类加载时将符号引用解析为直接引用。invokevirtual
:用于调用所有的虚方法。根据对象的实际类型进行分派(虚方法分派)。invokeinterface
:用于调用接口方法,会在运行时确定实现该接口的对象,并选择适合的方法进行调用。invokedynamic
:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。分派逻辑由用户设定的引导方法决定。
这些调用指令可以根据对象的类型和方法的特性进行不同的分派和调用。
invokedynamic
指令是在 JDK 7时加入到字节码中的,当时确实只为了做动态语言(如 JRuby、Scala)支持,Java 语言本身并不会用到它。而到了JDK 8 时代,Java 有了 Lambda 表达式和接口的默认方法,它们在底层调用时就会用到invokedynamic
指令。
其中,invokestatic
和 invokespecial
指令可以调用非虚方法,包括静态方法、私有方法、实例构造器和父类方法。而 invokevirtual
和 invokeinterface
指令用于调用虚方法,根据对象的实际类型进行分派。
也许这些指令看起来简单但很难理解,这是因为我们在上文多次提到过“方法调用”、“解析”、“分派”这些东西,别急,如果想要真正弄清楚这些指令,我们需要一步步来(
方法调用概述
方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。
一切方法调用在 Class
文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。
这个特性给 Java
带来了更强大的动态扩展能力,但也使得 Java
方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
在 Javav
中,方法调用过程中同时存在解析(Resolution)和分派(Dispatch)两个过程,方法调用过程中首先进行解析,将符号引用转化为直接引用,然后根据实际对象的类型进行分派,确定方法的实际执行版本。
解析
解析:
- 在类加载的解析阶段将方法调用的符号引用转化为直接引用的过程。
解析的前提:
- 方法在程序编写、编译阶段就有一个可确定的调用版本,并且这个版本在运行期是不可改变的。
- 必须是静态方法、私有方法和被
final
修饰的实例方法,因为它们都不可能通过继承或其他方式重写出其他版本。
解析调用过程:
- 解析调用是静态的过程,在编译期间就完全确定,不延迟到运行期再完成。
- 在类加载的解析阶段,涉及的符号引用会被转变为明确的直接引用,存储在常量池中。
这种转化使得方法调用在运行时可以更高效地执行,无需再进行符号解析,直接使用已经解析的直接引用。
分派(重点)
Java
作为一门面向对象的编程语言,具备继承、封装和多态这三个基本特征。
而分派调用过程在 Java
虚拟机中揭示了多态性的体现,特别是在方法的重载和重写方面:
- 重载(Overloading):重载是指在同一个类中定义多个方法,它们具有相同的名称但参数列表不同。在虚拟机中实现重载时,会根据方法调用的静态类型(声明类型)选择合适的方法版本。这属于静态分派,根据参数的静态类型来确定方法的版本。
- 重写(Overriding):重写是指子类重新定义父类中已有的方法,具有相同的名称和参数列表。在虚拟机中实现重写时,会根据方法调用的实际类型(运行时类型)选择合适的方法版本。这属于动态分派,根据实际对象的类型来确定方法的版本。
下面我们就来揭示 JVM
实现重载和重写的底层原理,这也是我们真正的重点部分。
静态分派
“分派”(Dispatch)这个词本身就具有动态性,一般不应用在静态语境之中,这部分原本在英文原版的《Java虚拟机规范》和《Java语言规范》里的说法都是“Method Overload Resolution”,即应该归入上节的“解析”里去讲解,但部分其他外文资料和国内翻译的许多中文资料都将这种行为称为“静态分派”。
为了解释静态分派和重载,我们看如下示例代码:
代码语言:javascript复制public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
上面的代码中定义了一个 StaticDispatch
类,包含了一个抽象类 Human
和两个继承自 Human
的子类 Man
和 Woman
。类中定义了三个重载的 sayHello
方法,分别接受 Human
、Man
和 Woman
类型的参数,并输出相应的问候语。
理论上我们重载了 sayHello()
方法,运行结果应该是:
hello,gentleman!
hello,lady!
但实际上控制台哼哼哼啊啊啊地输出了:
hello, guy!
hello, guy!
你先别急,让我先急