虚拟机如何执行字节码

本文深入探讨了虚拟机栈帧的概念及其在方法调用中的作用,详细讲解了局部变量表、操作数栈、动态连接和方法返回地址等核心组件的功能。同时,文章还分析了方法调用的解析过程,以及静态和动态分派在Java中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、概述

    虚拟机和物理机是相对应的,这两种机器都具有代码执行能力。物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统上的,而虚拟机的执行引擎是自己实现的,可以自行定制指令集和体系结构。

2、运行时栈帧结构

    栈帧是虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈中的栈元素。栈帧的概念结构如下:
在这里插入图片描述

    在编译期,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,栈帧分配的内存不会受到程序运行期变量数据的影响,只取决于虚拟机实现。
    只有位于栈顶的栈帧才是有效的,称为当前栈帧,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

2.1局部变量表

    局部变量表是一个变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位,Slot的大小未知,但是规范中说一个Slot能存放一个boolean、byte、char、short、int、float、reference、returnAddress,long和double则需要两个Slot的空间。

    在方法执行的时候,虚拟机通过局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法,那么第0个Slot用于传递this引用,其他参数按照参数列表顺序排列。

2.2 操作数栈

    操作数栈中的元素是java数据类型,在方法执行过程中,如果遇到数据操作,就会将待操作的数据进行出栈和入栈操作。例如计算3+2时,3和2就会在操作数栈的栈顶,当指令iadd运行的时候就将3和2出栈,然后相加,再将结果5入栈。

2.3 动态连接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用就是为了支持方法调用过程中的动态连接。
    字节码中的方法调用指令以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段就转化为直接引用,这叫静态解析。另一部分将在运行期间转化为直接引用,这叫动态连接。

2.4 方法返回地址

    方法退出后,都需要返回到方法被调用的位置。

3、方法调用

    方法调用阶段唯一关心的就是调用哪一个方法,而不是具体怎么执行。一切方法调用在Class文件里面存储的都只是符号引用,而不是在实际运行时内存布局中的入口地址(直接引用)。

3.1 解析

    所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种静态解析的前提是:待调用的方法在编译期就必须确定下来
    静态方法、私有方法、实例构造器、父类方法这4类方法在类加载的时候就会把符号引用转化为直接引用。它们是通过invokestatic和involespecial指令调用的。这两种调用方式是静态的,在编译期间就能完全确定。

3.2 分派与多态

(1) 静态分派 (也叫编译时多态,和方法重载有关)
    举一个例子,假设Man和Woman都继承了Human,
那么Human man = new Man();这个语句中,对象man的静态类型是Human,实际类型是Man。变量的静态类型是不会变的,而实际类型可能会以多态特性改变。
    下面有两个方法:

public void sayHello(Human man){
	System.out.println("hello guy");
}

public void sayHello(Man man){
	System.out.println("hello gentleman");
}

如果对man执行sayHello(man);
会打印hello guy,程序把man当作Human类型进行方法调用了。

原因: 编译器在重载的时候是通过参数的静态类型作为判断依据的,静态分派发生在编译阶段,和虚拟机无关。

(2) 动态分派 (也叫运行时多态,和方法重写有关)

String s1 = "ab";  
Object o = s1 +"c";  
String s = "abc";  
boolean b = o.equals(s);  

上述代码结果为true,因为o的实际类型是String,因此可以调用String的equals方法并比较内容。

JAVA语言支持静态的多分派和动态的单分派。
    在静态分派中,运行之前编译器不知道man的实际类型是什么,但是man是作为参数传递给其他方法的,但是代码都这样写了,总得把man安排一下吧,万一运行期间发现没有方法可执行就完了,因此编译器为了保险起见只能以它的静态类型为准,将它传递到类型匹配的方法中。将man作为参数调用sayHello()方法时,其实就是多分派,因为在运行之前必须为man选择一条稳妥的去向,要不然如果运行期间man现出真面目之后发现没有一个方法和它匹配就尴尬了,编译器选择了静态类型去安排man。
    在动态分派中调用o.equals()方法的时候,编译器还是不知道o的实际类型,但是这次是o调用自己的方法,是自己做决定,而不是作为参数参入别的方法,因此可以放到运行期间再去决定用哪种类型(万一它有自己的方法呢,还是先别随意安排吧),o的实际类型是String,String类中重写了Object的equals()方法,这样o有自己的equals方法,就不会去调用Object的方法了,如果String中没有重写,还是会调用Object的方法的。

4、基于栈的解释器执行过程

public class Main {
    public static void main(String[] args) {
        Main m = new Main();
        int c = m.add();
    }

    public int add(){
        int a = 5;
        int b = 6;
        return a+b;
    }
}

查看字节码指令为:

 public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Main
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method add:()I
      12: istore_2
      13: return

  public int add();
    Code:
       0: iconst_5
       1: istore_1
       2: bipush        6
       4: istore_2
       5: iload_1
       6: iload_2
       7: iadd
       8: ireturn
}

可见 4: invokespecial #3 处调用了Main类的默认构造器<init()>

9: invokevirtual #4 调用了add()方法
在add方法中,当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中,因此iconst_5将5压入操作栈中
istore_1将5出栈并保存到局部变量表的编号为1的Slot中,编号为0的Slot是this引用
bipush将6压入操作栈中
istore_2将6出栈并保存到编号为2的Slot上
iload_1将Slot1上的数(5)复制到操作栈顶
iload_2将Slot2上的数(6)复制到操作栈顶
iadd将栈顶的两个数5和6相加
ireturn将栈顶的数11返回给方法的调用者

     执行这些指令的时候,主要是 程序计数器、局部变量表、操作栈 在发生变化,
程序计数器会累加指令的总偏移量
局部变量表用于存放局部变量
操作栈用于对数据进行操作,运算指令都是对栈顶数据进行操作的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值