JVM_字节码:(栈帧与操作数栈)及(符号引用与直接引用)
- 栈帧(stack frame):
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构,归属于特定的一个线程,不存在并发的问题,本身是一种数据结构,实际上封装了方法的局部变量表、动态链接信息、方法的返回地址及操作数栈等信息。Java中方法的调用都存在栈帧的操作,存在入栈和出栈的操作。对于栈操作的简单理解:
3-2=1
3、2分别做入栈的操作,-出现分别出栈并计算出结果1 做入栈操作。
局部变量表:
- 用于存储局部变量,都是用(slot)来描述局部变量的最小单位,32位int类型的数据,都会占用一个slot 对于long类型用2个连续的slot表示。
- slot是可以复用的,对于10个局部变量可能存在的slot<10 方法体可以存在更小的作用域,方法体内部的局部变量的作用域是不同的,在局部变量表不作区分,当b、c占据的slot结束了生命周期后,可能会被d、e占据,如:
public void test(){
int a=3;
if(a>4){
int b=4;
int c=5;
}
int d=7;
int e=10;
}
动态链接是与C或者C++是不同的,对于C++来说Class之间的关系在编译期间就已经确定好了,包括地址的偏移量会提前设置好,所以存在动态链接库DLL Java是不同的,在编译期间Class之间方法的调用在加载或者在真正开始调用的时候才能确定,基于上述的方面存在符号和直接引用。
- 符号引用与直接引用
符号引用:对于目标类的,比如对于一个类的全局类名的描述,存放在常量池中的。
直接引用:是符合引用的内存地址,有时候在加载(或者第一次使用)的时候符号引用转换过来,有时候在【每次】在运行期间进行转换。分别称为静态解析(绑定)及动态连接。这种动态体现为Java的多态性。
Animal a=new Cat();
a.sleep();//invokevirtual
a=new Dog();
Java方法调用的字节码指令:(5种)
1、invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定调用实现该接口的哪一个对象的特定方法。(需要定位实现类)
2、invokestatic: 调用静态方法。
3、invokespecial: 可以自己的私有方法(注意私有方法是不可以被重写),也可以是构造方法(<init>),也可以调用父类的方法(成员或者构造器)
4、invokevirtual: 调用虚方法(C++中是存在的),是和多态紧密相关的 也是运行期动态查找的过程 查找继承这个类或接口的方法。
5、invokedynamic: 动态调用方法。(1.7引入的)可以调用动态语言比如javascript。不是讨论的重点。
Java源文件: 用于观察static方法的调用
public class MyTest4 {
public static void test(){
System.out.println("test invoked");
}
public static void main(String[] args) {
test();
}
}
反编译结果:
C:\spring_lecture\target\classes\com\compass\spring_lecture\binarycode>javap -v MyTest4
警告: 二进制文件MyTest4包含com.compass.spring_lecture.binarycode.MyTest4
Classfile /C:/spring_lecture/target/classes/com/compass/spring_lecture/binarycode/MyTest4.class
Last modified 2019-7-4; size 664 bytes
MD5 checksum 123e058e19e1836c1250aa6b13b35182
Compiled from "MyTest4.java"
public class com.compass.spring_lecture.binarycode.MyTest4
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // test invoked
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Methodref #6.#28 // com/compass/spring_lecture/binarycode/MyTest4.test:()V
#6 = Class #29 // com/compass/spring_lecture/binarycode/MyTest4
#7 = Class #30 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/compass/spring_lecture/binarycode/MyTest4;
#15 = Utf8 test
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 MyTest4.java
#22 = NameAndType #8:#9 // "<init>":()V
#23 = Class #31 // java/lang/System
#24 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#25 = Utf8 test invoked
#26 = Class #34 // java/io/PrintStream
#27 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#28 = NameAndType #15:#9 // test:()V
#29 = Utf8 com/compass/spring_lecture/binarycode/MyTest4
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public com.compass.spring_lecture.binarycode.MyTest4();
descriptor: ()V
flags: 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 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/compass/spring_lecture/binarycode/MyTest4;
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test invoked
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method test:()V 通过使用invokestatic调用
3: return
LineNumberTable:
line 15: 0
line 17: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
}
方法调用的解析过程:invokestatic invokespecial是在解析的过程中就可以确定的,静态及自身父类的构造及成员方法。
静态解析的4种情形:
- 静态方法
- 父类方法
- 构造方法
- 私有方法(公有方法会被重写或者复写 因此存在多态)
以上的4种方法就称为非虚方法,它们是在类加载阶段就可以将符号引用转换为直接引用。
方法重载(overload): 方法的参数类型或者参数的个数不同,签名的修饰符不算。
public class MyTest5 {
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 p1=new Father();
Grandpa p2=new Son();
MyTest5 myTest5=new MyTest5();
myTest5.test(p1);//grandpa
myTest5.test(p2);//grandpa
}
}
class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}
上面的例子涉及到方法的静态分派:
g1声明的类型是Grandpa 是静态类型 g1的真正指向的类型是Father(实际类型)。
我们可以得到这样的一个结论:
变量的静态类型是不会发生改变的,而变量的实际类型是可以发生变化的,是多态的一种体现,实际类型是在运行期间才可以确定的。
方法的重载是一种纯粹静态的一种行为,对JVM来说,是根据声明的参数,而不是根据实际类型来决定的,是根据静态类型来进行匹配的。是在编译期间就可以完全确定的。
看一下的字节码:
重载和重写:重载和重写是不同的,重载是静态的,重写是动态的。体现在静态类型上面。
重写的代码示例:
public class MyTest6 {
// apple
// orange
// orange
public static void main(String[] args) {
Furit apple = new Apple();
Furit orange = new Orange();
apple.test();
orange.test();
apple = new Orange();
apple.test();
}
}
class Furit {
public void test() {
System.out.println("fruit");
}
}
class Apple extends Furit {
@Override
public void test() {
System.out.println("apple");
}
}
class Orange extends Furit {
@Override
public void test() {
System.out.println("orange");
}
}
编译生成的字节码:
public class com.compass.spring_lecture.binarycode.MyTest6 {
public com.compass.spring_lecture.binarycode.MyTest6();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/compass/spring_lecture/binarycode/Apple
3: dup
4: invokespecial #3 // Method com/compass/spring_lecture/binarycode/Apple."<init>":()V
7: astore_1
8: new #4 // class com/compass/spring_lecture/binarycode/Orange
11: dup
12: invokespecial #5 // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
20: aload_2
21: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
24: new #4 // class com/compass/spring_lecture/binarycode/Orange
27: dup
28: invokespecial #5 // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
36: return
}
方法的动态分派:
从上述的字节码看,方法的字节码是一致的,但是从实际的调用上看,它们是不同的。
方法的动态分派涉及到一个重要的概念:方法的接收者,也就是方法的调用者
涉及到invokevirtual字节码指令的多态查找流程:
- 到操作数栈顶上第一个元素并寻找栈顶元素所指向的实际类型,并不是静态类型。
- 如果获取到并且访问权限也是ok的,就直接调用并返回,如果获取不到按照继承体系进行查找,找到就调用。
比较方法重写和方法重载,我们得到这样一个结论,方法重载是静态的,是编译期行为,方法重写是动态的,是运行期行为。
动态分派的一个最直接的例子是重写。对于重写,我们已经很熟悉了,那么Java虚拟机是如何在程序运行期间确定方法的执行版本的呢?
解释这个现象,就不得不涉及Java虚拟机的invokevirtual指令了,这个指令的解析过程有助于我们更深刻理解重写的本质。该指令的具体解析过程如下:
- 找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C
2 . 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常
-
如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
-
如果始终没有找到合适的方法,则抛出抽象方法错误的异常
从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。
虚方法表和动态分派机制:
针对于方法调用动态分派的过程:
虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table)简称vtable,
针对于invokeinterface指令来说,虚拟机会建立一个接口方法表的数据结构,(interface method table),简称itable。