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

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

时间:2023-05-17 19:14:20浏览次数:38  
标签:调用 java Utf8 void 栈帧 public 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虚拟机实现相关的一些附加信息,例如:对程序调试提供支持的信息。

标签:调用,java,Utf8,void,栈帧,public,JVM,方法,虚拟机
From: https://www.cnblogs.com/tod4/p/17409785.html

相关文章

  • JVM(三)运行时数据区概述及线程
    目录运行时数据区概述及线程简介线程间共享的说明JVM中的线程说明1程序寄存器ProgramCounterRegister为什么使用PC寄存器记录字节码指令地址?(为什么使用PC寄存器记录当前线程的执行地址)为什么程序计数器被设计成线程私有的运行时数据区概述及线程简介内存是硬盘和CPU的中间......
  • JVM(五)本地方法接口
    JVM(五)本地方法接口和本地方法栈1本地方法一个NativeMethod就是一个Java调用非Java代码的接口。在定义本地方法的时候,不提供实现体标识符native能够和除了abstract的java标识符连用publicclassNativeTest{ publicnativevoidmethod1()throwException; .........
  • JVM(四)虚拟机栈(三)虚拟机栈面试题
    JVM(四)虚拟机栈(三)虚拟机栈面试题1举例栈溢出的情况?当方法调用不停将栈帧压入虚拟机栈导致栈内空间不足而出现StackOverFlowError即是出现了栈溢出可以通过-Xss设置栈的大小,栈的大小可以是固定的也可以是动态变化的,如果固定且超出设定值则就会出现栈溢出;如果是动态变化的,栈空......
  • Putty连接虚拟机(在win11中安装的ubuntu20.04)提示: Network error: Connection refus
    #开启防火墙sudoufwenable#开启22号端口sudoufwallow22#重启防火墙sudoufwreload#查看状态sudoufwstatus#安装sshsudoaptinstallopenssh-server#尝试能否远程登录sshlocalhost......
  • 虚拟机计算机网络与物理机网络心得随笔
    虚拟机网络有三种模式:桥接模式、NAT模式、仅主机模式1.桥接模式虚拟机与物理机使用同一个网段,手写ip地址需要在写在同一个网段(什么是一个网段?192.168.31.xxx所有的这种都是一个网段)下面,子网掩码是为了掩盖、传递某些信息的,默认网关一个网段下面只有一个默认网关,所以往往子......
  • Android虚拟机的D盘储存
    大家知道安卓的模拟器位置默认是放在C盘的,这样比较占空间,可以通过创建·符号链接的方式来“欺骗”AS,从而创建到D盘:以管理员身份打开命令提示符,输入以下命令:mklink/DC:\Users\xxx\.android\avdD:\AndroidStudio\androidC\avd其中C:\Users\xxx\.android\avd是默认的安装......
  • 深入理解 python 虚拟机:多继承与 mro
    深入理解python虚拟机:多继承与mro在本篇文章当中将主要给大家介绍python当中的多继承和mro,通过介绍在多继承当中存在的问题就能够理解在cpython当中引入c3算法的原因了,从而能够帮助大家更好的了理解mro。python继承的问题继承是一种面向对象编程的概念,它可以让一......
  • 云原生背景下如何配置 JVM 内存
    背景前段时间业务研发反馈说是他的应用内存使用率很高,导致频繁的重启,让我排查下是怎么回事;在这之前我也没怎么在意过这个问题,正好这次排查分析的过程做一个记录。首先我查看了监控面板里的Pod监控:发现确实是快满了,而此时去查看应用的JVM占用情况却只有30%左右;说明并不是......
  • 5-1liunx虚拟机内存分配
    一、虚拟机硬件配置1.CPU:2核或更多2.内存:1G以上,推荐2G。3.硬盘:一块硬盘,200G。4.网卡:NAT模式。5.光盘:挂载对应版本的ISO文件。二、ISO下载地址:Centos http://mirrors.aliyun.com http://mirrors.sohu.com http://mirrors.163.comUbuntu https://cdimage.ubuntu.com......
  • 如何在虚拟机下的ubuntu制作磁盘阵列以及对几种磁盘阵列的解释
    一、磁盘阵列的分类raid0:一块硬盘及以上优点:数据读取快缺点:没有冗余能力,硬盘损坏,数据丢失raid1:至少两块硬盘优点:数据安全强,一块硬盘运行,另外一块硬盘做镜像备份数据。一块坏了,另外一块硬盘也有完整的数据,保障运行。缺点:做raid1之后硬盘使用率为50%.raid5:至少三块硬盘优点......