1 概念
1.1 栈帧(stack frame)
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构
栈帧本身是一种数据结构, 封装了方法的 局部变量表, 动态链接信息, 方法的返回地址以及操作数栈等信息;
1.2 符号引用,直接引用
符号引用
是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
直接引用
对于指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的本地指针。
指向实例变量、实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用可能是方法表的偏移量。
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用, 这种转换叫做 静态解析; 另外一些符号引用则是在每次运行期转换为直接引用, 这种转换叫做动态链接, 这体现为 Java的多态性;
2. invoke相关助记符
invokeinterface
调用接口中的方法,实际上是在运行期决定的, 决定到底调用实现该接口的哪个对象的特定方法;
invokestatic
调用静态方法;
invokespecial
调用自己的私有方法,构造方法() 以及父类的方法;
invokevirtual
调用虚方法, 运行期动态查找的过程;
invokedynamic
动态调用方法;
3. 静态解析的4种情形
- 静态方法
- 父类方法
- 构造方法
- 私有方法
以上4类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的
4. 方法重载与invokevirtual
字节码指令的关系
示例代码
public class MyTest3 {
//方法重载
public void test(Grandpa grandpa){
System.out.println("grandpa");
}
public void test(Father father){
System.out.println("father");
}
public void test(Son son){
System.out.println("son");
}
public static void main(String[] args) {
Grandpa g1 = new Father();
Grandpa g2 = new Son();
MyTest3 test3 = new MyTest3();
test3.test(g1);
test3.test(g2);
}
}
class Grandpa{}
class Father extends Grandpa{}
class Son extends Father{}
问
执行main方法,结果是什么?
结果
grandpa
grandpa
解析
方法重载,对于 JVM 来说是一种静态的行为; 其判定的唯一依据就是,根据方法形参的 静态类型 去匹配应该调用的具体的某一个方法
部分代码分析
Grandpa g1 = new Father();
方法的静态分派.
以上代码, g1 的静态类型是 Grandpa, 而g1的时机类型(真正指向的类型) 是Father.
我们可以得出这样一个结论: 变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定;
5 通过字节码分析 Java方法的静态分派与动态分派机制
5.1 示例分析
示例代码
public class MyTest4 {
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit orange = new Orange();
apple.test();
orange.test();
apple = new Orange();
apple.test();
}
}
class Fruit {
public void test() {
System.out.println("Fruit");
}
}
class Apple extends Fruit {
@Override
public void test() {
System.out.println("apple");
}
}
class Orange extends Fruit{
@Override
public void test() {
System.out.println("orange");
}
}
问
执行main方法,结果是什么?
结果
apple
orange
orange
解析
方法的动态分派
方法的动态分派涉及到一个重要概念:方法接收者,即该方法到底是由哪个对象去调用的
方法的动态分派涉及到invokevirtual
字节码指令的多态查找流程
invokevirtual
字节码指令的多态查找流程 :
1 首先会到操作数栈的栈顶 寻找栈顶元素所引用的对象的实际类型
2 在该实际类型的对象当中,如果寻找到了与常量池中描述符和名称都相同的方法,并且具备相应的访问权限,就会直接返回目标方法的直接引用
3 如果在实际类型的对象中没有找到该方法,那么就去其父类,继续执行该查找流程,直到找到,或者抛出异常
5.2 借助jclasslib工具查看字节码
最后边的助记符
前四个,0 new
,3 dup
,4 invokespecial
,astore_1
分别代表,创建新的对象,复制顶部操作数栈的值,调用构造方法,将对象引用赋给本地变量
上述四个助记符所执行的代码其实就是Fruit apple = new Apple();
而 8, 11, 12, 15这四个助记符则是,Fruit orange = new Orange();
第16个助记符,aload_1
,表示 Load reference from local variable,即 加载自本地变量的对象引用
下面invokevirtual
它是 ** 调用虚方法, 运行期动态查找的过程;**,我们看注释中显示的是 com/turnsole/myjvm/Fruit.test,这似乎是错误的,因为我们从执行结果中可以推论出apple.test();
实际调用的就是Apple
类的test()
方法,其实这里就涉及了上面我们说的👇
方法的动态分派
我们再重复一遍
方法的动态分派涉及到一个重要概念:方法接收者,即该方法到底是由哪个对象去调用的
方法的动态分派涉及到invokevirtual
字节码指令的多态查找流程
invokevirtual
字节码指令的多态查找流程 :
1 首先会到操作数栈的栈顶 寻找栈顶元素所引用的对象的实际类型
2 在该实际类型的对象当中,如果寻找到了与常量池中描述符和名称都相同的方法,并且具备相应的访问权限,就会直接返回目标方法的直接引用
3 如果在实际类型的对象中没有找到该方法,那么就去其父类,继续执行该查找流程,直到找到,或者抛出异常
针对于方法调用动态分派的过程, JVM会在类的方法区建立一个虚方发表的数据结构(virtual method table, vtable)
针对于invokeinterface
指令来说, JVM会建立一个叫做接口方法的数据结构(interface method table, itable)
看代码思考一个问题
class Parent{
void test1();
}
class Child extends Parent{
void test1();
void test2();
}
public class Test{
public static void main(String[] args){
Parent child = new Child();
child.test2();
}
}
思考以上代码有什么问题,从字节码角度分析
编译就会不通过
原因 : 在main
方法中child.test2();
代码,在字节码中会对应生成一个助记符invokevirtual
,该助记符,首先调用虚方法,该虚方法对应的就是对象child
的静态类型Parent
类中的test2()
方法,它是编译期行为.后续运行期会通过动态分派的方式查找到真正所需调用的方法. 然而,此时Parent
类中不存在方法test2()
, 因此编译器就不会通过!
5.3 结论
比较方法重载(overload) 与方法重写(overwrite), 我们可以得到这样一个结论:
方法重载是静态的,是编译期行为;JVM在操作具体方法的时候,是通过参数的静态类型进行的选择
方法重写是动态的,是运行期行为;JVM在操作具体方法的时候,是通过动态分派的形式进行的选择
6 字节码执行方式
6.1
现代JVM在执行Java代码的时候, 通常都会将解释执行与编译执行二者结合起来进行;
- 解释执行
所谓解释执行, 就是通过解释器来读取字节码, 遇到相应的指令就去执行该指令.
- 编译执行
所谓编译执行, 就是通过及时编译器(Just In Time, JIT) 将字节码转换为本地机器码来执行; 现代JVM会根据代码热点来生成相应的本地机器码;
6.2 基于栈的指令集与基于寄存器的指令集之间的关系
- JVM执行指令时所采取的方式是基于栈的指令集;
- 基于栈的指令集主要的操作有入栈与出栈两种;
- 基于栈的指令集的优势在于它可以在不同平台之间移植, 而基于寄存器的指令集是与硬件架构紧密关联的, 无法做到可移植;
- 基于栈的指令集的缺点在于完成相同的操作, 指令数量通常要比基于寄存器的指令集数量要多; 基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快很多.虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些;
6.3 JVM执行栈指令集实例剖析
public class MyTest5 {
public int myCalculate() {
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int result = (a + b - c) * d;
return result;
}
}
javap -verbose
反编译字节码内容
根据我们前面学到的知识,我们可以知道
stack即max_stack表示可操作数栈最大的深度,当前为2
locals即max_locals表示本地变量表最大数量,当前为6
args_size表示参数(形参)的个数,当前为1,即this
通过 jclasslib 工具我们看一下myCalculate()
方法的字节码内容,并挨个助记符解读
通过上面一段分析,我们确实可以再次体会下, 基于栈的指令集主要的操作有入栈与出栈两种;