文章目录
一、方法执行
以下面代码为例看一下执行引擎是如何将一段代码在执行部件上执行的,如下一段代码:
public class Math{
public static void main(String[] args){
int a = 1 ;
int b = 2;
int c = (a+b)*10;
}
}
其中main方法的字节码指令如下:
偏移量 | 指令 | 说明 |
---|---|---|
0: | iconst_1 | 常数1入栈 |
1: | istore_1 | 将栈顶元素移入本地变量1存储 |
2: | iconst_2 | 常数2入栈 |
3: | istore_2 | 将栈顶元素移入本地变量2存储 |
4: | iload_1 | 本地变量1入栈 |
5: | iload_2 | 本地变量2入栈 |
6: | iadd | 弹出栈顶两个元素相加 |
7: | bipush 10 | 将10入栈 |
9: | imul | 栈顶两个元素相乘 |
10: | istore_3 | 栈顶元素移入本地变量3存储 |
11: | return | 返回 |
对应到执行引擎的各执行部件如图:
当前线程在执行方法之前,PC寄存器存储的是第一条指令的地址,局部变量表和操作数栈都是空的,执行完前4条指令,将a,b两个局部变量赋值,也就是将1,2两个常量分别存入1号和2号局部变量区(0号存储this指针)。如图:
第5条和第6条指令分别是将两个局部变量入栈,PC寄存器存储第七条指令的地址6,如图:
接着执行第七条指令将栈顶的两个元素弹出后相加,结果再入栈,如图:
可以看出,变量a和b相加的结果3存在当前栈顶中,接下来第8条指令将10入栈,如图:
当前PC寄存器执行的地址是9,下一个操作是将当前栈的两个操作数弹出进行相乘并把结果压入栈中,
第10条指令是将当前的栈顶元素存入局部变量3中,如图:
第10条指令执行完后栈中元素出栈,出栈的元素存储在局部变量区3中,对应的是变量c的值。最后一条指令是return,这条指令执行完后当前的这个方法对应的这些部件会被JVM回收,局部变量区的所有值将全部释放,PC寄存器会被销毁,在Java栈中与这个方法对应的栈帧将消失。
二、方法调用
2.1 方法绑定
方法绑定指的是将方法与类进行绑定
- 静态绑定(前期绑定、编译时绑定):private、static、构造方法、super方法。
- 动态绑定(后期绑定、运行时绑定):存在类型多态。只有运行的时候,才能确定最终的形态,确定最终的方法是属于哪个类的方法(方法重写)。动态绑定是多态性得以实现的重要因素,它通过方法表来实现。包括非私有的实例方法和接口方法
- 方法重写(面向对象多态的重要体现):子类和父类出现同名同参数(参数顺序、个数、类型都相同)非static方法
Father f = new Son();//编译看左边、运行看右边
f.say();
2.2 方法表(虚函数表)
- JVM 会在链接类的过程中,给类分配相应的方法表内存空间。每个类对应一个方法表。这些都是存在于方法区中的。这里与 C++略有不同,C++中每个对象的第一个指针就是指向了相应的虚函数表。而 Java 中每个对象的对象头有一个类型指针,可以索引到对应的类,在对应的类数据中对应一个方法表。也就是C++的方法表是对象级别的,而Java的方法表是类级别的。
- 这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
- 方法表的好处:利用空间换时间的思维,去优化确定目标方法的查找时间。
class Foo {
@Override
public String toString() {
return "Foo";
}
void run(){}
}
它的虚函数表如下:
2.3 方法是如何被JVM识别的?
Java虚拟机识别方法的关键在于【类名 + 方法名 + 方法描述符(method descriptor)】。
注:方法描述符由方法的【参数类型 + 返回类型】构成。
2.4 方法是如何调用的?
静态绑定的方法是如何被找到的?
- 找到A类
- 找到A类存储的方法指针,去找到方法对象
动态绑定的方法是如何被找到的?
- 根据调用者的动态类型(栈的局部变量表中)
- 根据动态类型,找到方法区该类型的方法表
- 在方法表中找到具体的方法指针,找到方法对象
找到之后,调用相应的方法指令。
2.5 方法调用的字节码指令
在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析将这些方法的符号引用替换为了直接引用,再在栈中执行这些指令。
2.5.1 invokevirtual
最常见的方法调用类型是invokevirtual,它对应的是虚分派(virtual dispatch)。当你通过invokevirtual来调用一个实例方法时,JVM会查询虚函数表(vtable),如果vtable中没有这个方法的定义,JVM会通过类元信息指向父类的指针来继续查找。
这个过程就是JVM中方法重写(override)的基本思路。为了使得这个过程更加高效,虚函数表的排列方式比较特殊。每个vtable都会把父类中定义过的方法放在虚函数表的起始处。并且这些方法的排列顺序和父类是严格一致的。而这个类型所新增的独有方法会放在虚函数表的尾部。
2.5.2 invokeinterface
invokeinterface就会有一些复杂了。接口方法在虚表中的实现的位置就不是固定的了。 接口方法的偏移量不同是由于它们的类继承结构是不同的。一个编译时只知道接口类型的对象,当调用它上面的方法时,就会增加额外的查找工作。先在本类的vtable查找,如果没有实现该方法,就到父类中查找,这时由于偏移量不同,需要在父类的vtable重新查找一遍。
2.5.3 invokespecial&invokestatic
invokespecial用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic用于调用静态方法。这些方法在vtable中没有地址引用,需要在类元数据的方法信息区查找。
2.5.4 invokedynamic
在使用了invokedynamic的类的常量池中,会有一个特殊的常量,invokedynamic操作码正是通过它来实现这种灵活性的。这个常量包含了动态方法调用所需的额外信息,它又被称为引导方法(Bootstrap Method,BSM)。这是invokedynamic实现的关键,每个invokedynamic的调用点(call site)都对应着一个BSM的常量池项。为了将BSM关联到某个特定的invokedynamic调用点上,Java 7的类文件格式中新增了一个InvokeDynamic类型的新常量池项。它包含了一个方法句柄,会指向最终绑定到这个调用点上的方法。
nvokedynamic指令的调用点在类加载时是“未链接的(unlaced)”。调用BSM后才能确定具体要调用的方法,返回的这个CallSite对象会被关联到调用点上。