JVM(四)虚拟机栈(二)栈帧结构:动态链接、方法返回地址与附加信息

JVM(三)虚拟机栈(二)栈帧结构:动态链接、方法返回地址与附加信息


1 动态链接技术

  • 每一个栈帧,都包含着一个指向运行时常量池该指针所属方法的引用,即方法区中的方法地址,包含该引用的目的就是为了支持当前方法能够实现动态链接。所以动态链接又称为运行时常量池中的方法引用
  • 在java源文件被编译为字节码文件中,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池中。动态链接的作用就是把这些符号应用转换为调用方法的直接引用。如描述一个方法调用了其他的方法,就是通过常量池中指向方法的符号引用来表示的。

image-20230307193757950

public class DynamicLinkingTest {
    int num;
    public void A() {

    }

    public void B() {
        A();
        num++;
    }
}
  Constant pool:
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = Methodref          #4.#20         // com/hikaru/java/DynamicLinkingTest.A:()V
   #3 = Fieldref           #4.#21         // com/hikaru/java/DynamicLinkingTest.num:I
   #4 = Class              #22            // com/hikaru/java/DynamicLinkingTest
   #5 = Class              #23            // java/lang/Object
   #6 = Utf8               num
   #7 = Utf8               I
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/hikaru/java/DynamicLinkingTest;
  #15 = Utf8               A
  #16 = Utf8               B
  #17 = Utf8               SourceFile
  #18 = Utf8               DynamicLinkingTest.java
  #19 = NameAndType        #8:#9          // "<init>":()V
  #20 = NameAndType        #15:#9         // A:()V
  #21 = NameAndType        #6:#7          // num:I
  #22 = Utf8               com/hikaru/java/DynamicLinkingTest
  #23 = Utf8               java/lang/Object
}
  ...
  public void B();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method A:()V
         4: aload_0
         5: dup
         6: getfield      #3                  // Field num:I
         9: iconst_1
        10: iadd
        11: putfield      #3                  // Field num:I
        14: return
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/hikaru/java/DynamicLinkingTest;
}

  • 在上面的方法B中 invokevirtual #2,指向了 Constant pool的 #2 = Methodref(方法引用) #4.#20,再经过一系列引用指向了方法A。

  • 可以关注一下常量池中加载的东西:

    • Object父类
    • 常量:#6 = Utf8 num
    • 字面量
    • 类型引用
    • 方法引用
image-20230307201959529
  • 如上,运行的时候就会将字节码文件中的常量池加载到方法区的常量池
为什么需要运行时常量池?

字节码文件需要很多数据的支持,不可能把所有的数据都写入字节码文件,通过动态链接技术可以动态地引用相关结构。常量池的作用,就是为了提供一些符号和常量,便于指令的识别,减少字节码文件的大小。

2 方法的调用


2.1 静态链接、静态链接

​ 在JVM中,将符号引用转换为方法的直接引用与方法的绑定机制有关:

  • 静态链接:当一个字节码文件被装载进入JVM的时候,如果被调用的目标方法在编译期间可知,且保持运行期间不变,则可以直接将符号引用转换为方法的直接引用,这个过程称作静态链接,也称作早期链接

  • 动态链接:如果被调用的目标方法无法在编译期间确定下来,只能在运行期间才能确定再将符号引用转换为方法的直接引用,这个过程称作动态链接,也称作晚期链接

  • 面向过程语言都是早期绑定,面向对象的语言出现了多态因而变成了晚期绑定

  • Java中的任何普通方法都具有虚函数的特征,虚函数即使C++中的概念,使用virtual关键字标识的方法能够被父类对象所引用,以此来实现多态;Java中的方法如果不希望拥有虚函数的特征,则可以使用final关键字标识,表明不能被子类调用

class Animal {
    public void eat() {

    }
}

interface Huntable {
    void hunt();
}


class Dog extends Animal implements Huntable {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {
        System.out.println("狗拿耗子,多管闲事");
    }
}

class Cat extends Animal implements Huntable {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("猫吃耗子,天经地义");
    }
}

public class AnimalTest {
    public void showAnimal(Animal animal) {
        // 表现为晚期绑定
        animal.eat();
    }

    public void showHunt(Huntable huntable) {
        // 表现为晚期绑定
        huntable.hunt();
    }
}

​ 查看AnimalTest的字节码文件的两个方法,一个为invokevirtual,一个为invokeinterface,两个均表现为晚期绑定,即编译期间不能够确定只能运行期间确定的方法(一个因为多态另一个因为接口实现)

image-20230517132822050 image-20230517132845536

​ 如果在cat类中编写调用父类的构造函数,则表现为早期绑定,因为编译期间能够确定该方法

class Cat extends Animal implements Huntable {
    public Cat() {
        super();
    }
    public Cat(String name) {
        this();
    }

    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("猫吃耗子,天经地义");
    }
}
image-20230517133328983
2.2 方法调用指令区分非虚方法和虚方法

虚方法与非虚方法:

  • 非虚方法:如果方法在编译期间就确定了具体的调用版本,并且在运行时是不可变的,则这种方法是非虚方法

  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法

    虚方法和多态有密切的关系,对象的多态性有两个使用前提:

    1. 存在类继承关系
    2. 进行了方法的重写

    而静态方法、私有方法、final修饰的方法和实例构造器都是不能被重写的,因此被调用一定确定是子类调用的,即为非虚方法,而父类方法也是编译期间就能确定的

  • 其他方法都是虚方法

2.3 虚拟机提供的方法调用指令
  • 普通调用指令
    • invokeStatic:调用静态方法
    • invokeSpecial:调用<init>方法、私有方法以及父类方法
    • invokeVirtual:调用虚方法以及final方法
    • invokeInterface:调用接口方法
  • 动态调用指令:
    • invokeDynamic:动态解析出要调用的方法然后执行

​ 普通调用指令固化在虚拟机内部,方法的调用认为不可干预,而invokeDynamic由用户确定方法的版本。并且invokeStatic、invokeSpecial指令调用的方法称作非虚方法,后两种除了final方法被调用的方法都是虚方法

class Father {
    public Father() {
        System.out.println("Father的构造器.");
    }

    public static void showStatic(String str) {
        System.out.println("Father " + str);
    }

    public final void showFinal() {
        System.out.println("Father show final.");
    }

    public void showCommon() {
        System.out.println("Father show common.");
    }

}

public class Son extends Father{
    public Son() {
        super();
    }

    public Son(int age) {
        this();
    }

    // 静态方法不能被重写
    public static void showStatic(String str) {
        System.out.println("Son " + str);
    }

    private void showPrivate(String str) {
        System.out.println("Son private " + str);
    }
    
    @Override
    public void showCommon() {
        
    }

    public void show() {
        // invokeStatc,静态方法不能被重写,因此编译期间可以确定是子类的,所以是非虚方法
        showStatic("abc");
        // invokeStatc,父类方法
        Father.showStatic("def");
        // invokeSpecial,私有方法
        showPrivate("hello");
        // invokeSpecial,父类方法
        super.showCommon();
        // invokeVirtual,final方法
        showFinal();
        // invokeSpecial,父类方法
        super.showFinal();
        
        
        //invokevirtual 虚方法
        showCommon();
        //invokevirtual 虚方法
        info();

        MethodInterface methodInterface = null;
        // invokeinterface
        methodInterface.methodA();
    }
    public void info() {

    }

    public static void main(String[] args) {
        Son son = new Son();
        son.show();
    }
}
interface MethodInterface {
    void methodA();
}

2.4 invokedDynamic动态调用指令
  • java7中开始引入invokedDynamic指令,目的是为了实现支持动态类型语言
  • 直到java8的Lambda表达式的出现,在java中才有了直接实现invokedDynamic的方式
  • 静态语言和动态语言的区别在于:
    • 静态语言是在编译期间进行类型检查,而动态语言则是在运行期间
    • 静态语言是判断变量自身的类型信息,而动态语言是判断变量值的类型信息,变量本身没有类型信息
    • Func f = s ->{return true;}就是典型的右边变量值确定类型信息
// 标识只能够使用lambda表达式实现函数,否则编译报错
@FunctionalInterface
interface Func {
    boolean func(String str);
}

public class Lambda {
    public void lambda(Func func) {

    }

    public static void main(String[] args) {
        Lambda lambda = new Lambda();
        Func f = s -> {
          return true;
        };
        lambda.lambda(f);
        lambda.lambda(s ->{
            return true;
        });
    }
}
image-20230517150939911
2.5 方法重写的本质与虚方法表的使用
方法重写的本质:动态分配
  1. 方法执行过程中,栈帧中的操作数栈顶的第一个元素对应的执行对象的实际类型,记作C
  2. 如果在过程结束,如果能够在C对应的常量池中找到描述符简单名称都相符的方法,则进行访问权限校验,校验通过则返回这个方法的直接引用,否则返回java.lang.IllegalAccessError异常
  3. 否则则按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证工作
  4. 如果始终没有找到,则抛出java.lang.AbstractMethodError异常,认为这个方法是个接口方法
虚方法表
  • 在面向对象的编程中,对于重写方法会频繁使用到上面的动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话可能会影响到执行效率。
  • 因此JVM在类方法区建立了一个虚方法表(Virtual method table)使用索引来代替查找
  • 每个类都有一个虚方法表,表中存放着各个方法的实际入口
  • 虚方法表会在类加载阶段链接阶段被创建并开始初始化(具体是类加载过程的链接的解析阶段

虚方法表中只包含虚方法而不包含非虚方法,因为非虚方法在编译期间就能够确定

举个栗子:

image-20230517155339996
  • CockerSpaniel的虚方法表为(CockerSpaniel重写了父类的sayHello以及sayGoodBye()方法,因此这两个指向自身的方法引用,没有重写toString()则向上查找指向Dog的toString()方法符号引用,Object的方法同理):

    image-20230517155456189

看到弹幕有问为什么这些父类方法会出现在虚方法表?

因为这些是重写的方法,只有显示调用super.xxx()的时候才表名这是一个确定的父类方法,这时候才是非虚方法,总之能够被重写的方法、具有多态特征导致编译不能确认是子类还是父类的方法都是虚方法

3 方法的返回地址

  • 方法的返回地址存放的就是PC寄存器的值,也就是下一条需要执行的字节码指令的地址

    这里PC寄存器的地址对应的字节码指令主要有以下几种:

    正常完成:

    • ireturn:返回值为boolean、byte、char、short、int
    • lreturn:返回值为long类型
    • dreturn:返回值为double类型
    • areturn:返回值为引用类型
    • return:返回值为void、实例初始化方法<cinit>、类和接口的初始化方法<init>

    出现未处理的异常,非正常退出:

    • 需要先在异常处理表中搜索匹配的异常处理器,没有找到就会异常退出;如下表示如果在字节码4-16行出现异常,就按照19行的catch进行处理,异常类型为任意

      image-20230517162630374

  • 本质上方法的退出就是当前栈帧出栈的过程。因此需要恢复上层方法的局部变量表、操作数栈(将返回值压入操作数栈的栈顶)设置PC寄存器等,以让调用者继续执行下去

image-20230517160212135

4 一些附加信息

​ 栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如:对程序调试提供支持的信息。

posted @ 2023-05-17 18:55  Tod4  阅读(129)  评论(0编辑  收藏  举报