字节码栈帧,方法分派,方法的overload与overwrite的区别,虚方法表注意事项

栈帧(stack frame)

定义

  • 栈帧(stack frame):(每个栈帧都是由特定的线程执行的 不存在并发情况
  • 栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构
  • 栈帧本身是一种数据结构,封装了方法的局部变量表,动态链接信息,方法的返回地址以及操作数栈等信息。(由此可以看出方法的局部变量是封装到栈当中并不是封装到堆当中)
  • 字节码在执行method的code属性里面的操作其实就是执行栈指令
  • 每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡;栈帧属于虚拟机栈的数据,一个栈中可以有多个栈帧,栈帧随着方法的调用而创建,随着方法的结束而消亡(线程一对一虚拟机栈,虚拟机栈一对多栈帧)。
符号引用,直接引用
  • 符号引用指jvm在编译器就能在常量池找到对应的数据,直接引用则指是在编译器动态链接
  • 有些符号引用,是在类加载阶段或者是第一次使用会转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为java的多态性

五种方法调用助记符类型(重要)

  • invokeinterface: 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法。
  • invokestatic: 调用静态方法。
  • invokespecial: 调用自己的私有方法,构造方法() 以及父类的方法。
  • invokevirtual: 调用虚方法,运行期动态查找的过程。
  • invokedynamic: 动态调用方法

上述助记符当中2和3 对应的四种方法类型 字节码在解析的时候就能唯一确定调用过程此过程就能将符号引用转化为直接引用

非虚方法
  • 静态解析的四种情形:
    1.私有方法
    2.静态方法
    3.构造方法
    4.父类方法
    上述四种方法称为非虚方法,此过程就能将符号引用转化为直接引用;都有稳定性无法被重写和继承就无法有动态性

栈帧储存方法

栈帧中slot 存储局部变量的最小单位
1.占据32个字节
2.可复用 每个局部变量作用域不相同 所有生命周期也不相同 局部变量表不会区分这一点 也就是说当 cd生命周期结束其占据的slot位置就有可能被fg占用

public class MyTest4 {
    /**
     * 栈帧中slot 存储局部变量的最小单位
     * 1.占据32个字节
     * 2.可复用 每个局部变量作用域不相同 所有生命周期也不相同 局部变量表不会区分这一点
     * 也就是说当 cd生命周期结束其占据的slot位置就有可能被fg占用
     */
    public void test() {
        int a = 3;
        if (a < 4) {
            //当 cb 离开花括号生命周期就结束了 就会被垃圾回收器回收
            int c = 5;
            int b = 6;
        }
        int f = 23;
        int g = 24;
    }

}

方法的分派

静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,如重载

方法的重载
/**
 * 方法的静态分派。
 *
 * Grandpa g1 = new Father();
 * 编译期 静态类型是Grandpa 这个过程是静态绑定 子指向父类型是个静态绑定
 * 以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father.
 *
 * 我们可以得出这样一个结论:变量的静态类型是不会发生变化的,
 * 而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期可确定。
 */
public class MyTest5 {

    /**
     *     对于jvm来说 方法的重载,是一种静态的行为
     *     当jvm在编译期时就能确定, 重载参数类型由传过来的参数类型所决定
     *     如g1,g2 都是Grandpa 类型
     *
     */
    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();
        MyTest5 myTest5 = new MyTest5();
        myTest5.test(g1);
        myTest5.test(g2);
    }
}

class Grandpa {

}

class Father extends Grandpa {

}

class Son extends Father {

}

打印结果

grandpa
grandpa

由上可知
Grandpa g1 = new Father();
以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father
我们可以得出这样一个结论:变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期可确定。

方法的重载

对于jvm来说 方法的重载,是一种静态的行为 当jvm在编译期时就能完全确定重载方法对应的类它的所有方法都能确定, 重载参数类型由传过来的参数类型所决定 如g1,g2 都是Grandpa 类型

动态分派

我们从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

public class MyTest6 {

    public static void main(String[] args) {
    	//此时编译期间apple orange 字节码识别的类型都是Fruit  
    	//如果静态期间发生将该变量赋值或者传递不去调用他们自身的方法
    	//此时传递调用的都是 Fruit  静态类型
        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  peach extends Fruit{
    @Override
    public void test() {
        System.out.println("peach");
    }
}
class Apple extends peach { //当我不重写test他会从我父类进行查找
//    @Override
//    public void test() {
//        System.out.println("Apple");
//    }
}

class Orange extends Fruit {
    @Override
    public void test() {
        System.out.println("Orange");
    }
}

打印

peach
Orange
Orange

main方法生成的字节码文件

new 完成三件事 
1.堆中开辟空间
2.完成方法构造
3.将堆中实例地址返回
//实例对象
 0 new #2 <com/example/demo/com/jvm/bytecode/Apple>
 //对象值压入栈顶
 3 dup
 //完成方法构造
 4 invokespecial #3 <com/example/demo/com/jvm/bytecode/Apple.<init>>
 //地址赋值局部变量数组对应1的索引
 7 astore_1
 8 new #4 <com/example/demo/com/jvm/bytecode/Orange>
11 dup
12 invokespecial #5 <com/example/demo/com/jvm/bytecode/Orange.<init>>
15 astore_2
//从局部变量当中加载序号为1的应用就是Apple
16 aload_1
//调用虚方法动态的执行  
1.先从实际类型中查找对应name方法描述 返回参数相同的 方法 找到直接方法
2.没有直接从下往上往父类找 直到找到未知 没找到抛出异常
17 invokevirtual #6 <com/example/demo/com/jvm/bytecode/Fruit.test>
20 aload_2
21 invokevirtual #6 <com/example/demo/com/jvm/bytecode/Fruit.test>
24 new #4 <com/example/demo/com/jvm/bytecode/Orange>
27 dup
28 invokespecial #5 <com/example/demo/com/jvm/bytecode/Orange.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/example/demo/com/jvm/bytecode/Fruit.test>
36 return

结论

方法的动态分派
方法的动态分派涉及到一个重要概念:方法接受者。

  • invokevirtual字节码指令的多态查找流程他有对应的一个参数用来从实际类型中去查找与其相符的方法

比较方法重载(overload)与方法重写(overwrite),我们可以得到这样一个结论:
方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为。

方法的重写(overWrite)

Fruit apple = new Apple();
apple.test();
由上述看出方法的重写是由对象本身去调用他自己对应的test方法 invokevirtual 会在运行期根据实际类型去依次由子类往上到父类中查找到 与助记符(17)对应的test 名称 返回值 描述符 访问权限一样的方法 找到就返回找不到就抛异常 。当找到之后他就会将对应的 符号引用转换成对子类的直接引用

重写和重载的区别

Mytest5对应的调用只是参数传递不同 但是参数的静态类型相同 编译器已经确定 ,而Mytest6当中他们是本身对应的实例不同并且每个实例调用的是它对应的不同方法 需运行期动态链接

虚拟机动态分派的实现

/**
 * 针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable)
 * 针对invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,itable)
 */


public class MyTest7 {

    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal dog = new Dog();

        Fruit fruit = new Apple();
       
        animal.test(fruit);
     
        dog.test(new Apple());
    }

}

class Animal {
    public void test(Fruit str) {

        System.out.println("animal Fruit");
    }

    public void test(Apple date) {
        System.out.println("animal Apple");
    }
}

class Dog extends Animal {

    @Override
    public void test(Fruit date) {
        System.out.println("dog Fruit");
    }

    @Override
    public void test(Apple str) {
        System.out.println("dog Apple");
    }
}

打印

 //此时属于静态分派
animal Fruit
//动态分派先去 DOg去找对应的方法找到了
dog Apple

当我将Dog中的 test(Apple str)去掉

animal Fruit
//动态分派先去 DOg去找对应的方法 找不到来父类找
animal Apple

此时我们将Animal 中的test(Fruit str) 去掉此时程序报错说明此时编译期间就已经能定位到该方法
在这里插入图片描述

静态分派和动态分派的区别
  • 静态分派是在编译器就能在常量池查找到对应数据,且将符号引入直接转换成直接引用也叫做静态解析
  • 动态分派是在编译器能找到对应静态类型全类名,此时需要再运行期找到对应实际类型执行的类别遵循invokevirtual规则 找对对应数据数据此时将符号引用转换成对对应类的直接引用

虚方法表

前面介绍的分派过程,作为对虚拟机概念模型的解析基本上已经足够了,它已经解决了虚拟机在分派中"会做什么"这个问题。

但是,虚拟机”具体是如何做到的“,可能各种虚拟机实现都会有些差别。

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。面对这种情况,最常用的稳定优化手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable),使用虚方法表索引来代替元数据查找以提高性能
在这里插入图片描述

  • 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实际入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实际版本的入口地址。
  • 为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中具有一样的索引序号,这样当类型变换时,仅仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址。(也就是子类继承父类的方法索引跟父类的是相同的。当子类查找到(如索引为5的方法不正确)则直接去父类找对应索引为5的方法,不用全盘扫描
  • 方法表一般在类加载阶段的连接第二个阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕
方法查看问题

方法表与类加载器类似:只记录当前类与其父类的方法,就会存在无法找到对应方法就会报错,此时需要使用强制类型转换将父类转换成子类

而在字节码当中就存在如下助记符

19 invokevirtual #7 <com/example/demo/com/jvm/bytecode/Animal.method>

如上的指令去指向静态类型的method方法 显然是无法找到

例如 我们将MyTest7 的 Dog类新增一个method方法

class Dog extends Animal {

    @Override
    public void test(Fruit date) {
        System.out.println("dog Fruit");
    }

    @Override
    public void test(Apple str) {
        System.out.println("dog Apple");
    }
    public void method(Apple str) {
        System.out.println("dog Apple");
    }
}

此时就会报错父类无法查看子类新增的方法 ,需要向下类型转换
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值