虚拟机字节码执行引擎

本文介绍了Java虚拟机中方法调用的过程,包括栈帧结构、局部变量表、操作数栈等内容。详细解释了静态分派与动态分派的区别及其实现原理,还介绍了invokedynamic指令以及MethodHandle的应用。

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

1:概述

    在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这种概念模型成为各种虚拟机执行引擎的统一外观(Facade)。不同的虚拟机,可能通过解释器执行或者编译器产生本地代码执行,或者包含几个不同级别的编译器执行引擎。

2:运行时栈帧结构

    栈帧是用户支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。

2.1:局部变量表

    局部变量表是一组变量值存储空间,用户存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指定slot占用的空间大小,只有很导向性的说明每个slot都应该能存放下一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8中数据类型都可以用32位,或者更小的物理内存来存放。
    对于64位的数据类型,虚拟机会采用高位对齐的方式为其分配两个连续的slot空间。Java语言中明确的64位的数据类型只有long和double两种。
    为了尽可能的节省栈帧空间,局部变量表中的slot是可以重用的。

2.2:操作数栈

    在一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。

2.3:动态连接

    每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了方法调用过程中的动态连接。

2.4:方法返回地址

    方法返回相当于方法退出,正常执行可通过PC计数器完成,异常情况需要异常处理器来确定方法退出。

2.5:附加信息

    虚拟机规范运行虚拟机实现增加一些规范中没有描述的信息到栈帧中。

3:方法调用

    方法调用阶段唯一的任务就是确定调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

3.1:解析

    在类加载的解析阶段,会将其中一部分的符号引用转化为直接引用,这种解析能成立的前提是:方法在程序执行前就有可确定的版本,并且在运行期间不会发生变化。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析。在Java语言中,符合“编译期可知,运行期不可变”的要求,主要有两种类型的方法,静态方法和私有方法。

3.2:分派
  • 静态分派:
/**
 * 下面这段程序的运行结果是
 * Hello gay!
 * Hello gay!
 **/
public class StaticDispatch {
    static abstract class Human{
    }
    static Man extends Human{
    }
    static class Woman extends Human{
    }

    public void sayHello(Human guy){
        System.out.println("Hello guy!");
    }

    public void sayHello(Man guy){
        System.out.println("Hello Man!");
    }

    public void sayHello(Woman guy){
        System.out.println("Hello lady!");
    }
    public static void main(String [] args){
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

    先解释 Human man = new Man();Human称为变量的“静态类型”,或者叫做“外观类型”,静态类型和实际类型都可以在程序中发生一些变化,区别静态类型的变化仅仅在使用时发生,变量本身的静态类型不会发生变化,并且最终的静态类型在编译期可知的;而实际类型变化的结果在运行期才可以确定。例如下面的代码:

// 实际类型变化
Human man = new Man();
man = new Woman();
// 静态类型变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);

    静态分派的典型应用是代码重载,当重载的方法很多时,编译器会自动选择最合适的方法。

  • 动态分派
        和多态的重写(Override)有密切关系,在子类中的重写父类的方法,会直接调用子类的实现。

  • 单分派和多分派
        方法的接受者和方法的参数统称为方法的 宗量。到今天为止,Java语言是一个静态多分派和动态单分派的语言。

  • 虚拟机动态分派的实现
        使用虚拟方法表实现动态分派。方法表一般在类的连接阶段初始化,准备了类的变量初始化值后,虚拟机会把该类的方法表也初始化完成。除了这个方法,还可以使用“内联缓存”和“类型继承关系分析”技术的“守护内联”的两种比较激进的方法实现。

3.3:动态类型语言支持

    Jdk1.7多了一个字节码指令集—invokedynamic指令,它的出现是实现动态类型语言支持的改进之一。

  1. 动态类型语言:动态语言的关键特征是它的类型检查的主体过程是在运行期还不是在编译期,满足这个特征的语言有很多,常用的包括:APL、 Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、 Prolog、Python、Ruby、SmallTalk等。
  2. JDK1.7与动态类型:为了支持动态语言,Java开发了invokedynamic指令和java.lang.invoke包。
  3. **java.lang.invoke:**MethodHandle方法提供了类似于函数指针或者委托的方法别名的工具。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import static java.lang.invoke.MethodHandles.lookup;

/**
 1. Method Handle 基础用法演示
 */
public class MethodHandleTest {

    static class ClassA {

        public void println(String s) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        System.out.println(obj.getClass());
        // 无论obj是哪个实现类,都能正确调用到println方法
        getPrintlnMH(obj).invokeExact("testtest");
    }

    /**
     * 模拟了invokevirtual指令的执行过程
     **/
    private static MethodHandle getPrintlnMH(Object reveiver) throws NoSuchMethodException, IllegalAccessException {

        // Method代表方法类型,包含的方法的返回值和具体参数
        MethodType methodType = MethodType.methodType(void.class, String.class);

        /**
         *  lookup方法来自与MethodHandles.lookup,
         * 这句话是在指定的类中查找符合给定的方法名称,方法类型,并符合指定调用权限的方法句柄
         * 因为这里调用的是虚方法,按照Java语言的规则,方法的第一个参数是隐式的,代表该方法的
         * 接受者,也即是this指定的对象,这个参数以前是放在参数列表中进行传递的,而现在提供了
         * bindTo()方法来完成这件事情。
         */
        return lookup().findVirtual(reveiver.getClass(), "println", methodType).bindTo(reveiver);
    }
}

    MethodHandle和反射Reflection的区别:
    从本质上讲,Reflection和MethodHandle都是在模拟方法调用,MethodHandle模拟的是字节码层面的方法调用,而Reflection是模拟 Java代码层次的方法调用。
    Reflection包含的信息很多,是Java一端的全面映射;所以MethodHandle是轻量级的,而Reflection是重量级的。
    由于MethodHandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面的各种优化(如方法内联),在MethodHandle上也应当采用类似的思路去支持(但不完善)。

4. invokedynamic指令:虚拟机实现动态调用的关键指令。
5. 掌控方法分派规则:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import static java.lang.invoke.MethodHandles.lookup;

/**
 * 使用MethodHandle解决访问祖父的方法
 */
public class MethodHandleGrandFather {
    class GrandFather {
        void thinking() {
            System.out.println("i am grand father");
        }
    }

    class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father2");
        }
    }


    class Son extends Father {
        /**
         * 我想调用我祖父的thinking方法
         */
        void thinking() {
            MethodType mt = MethodType.methodType(void.class);
            try {
                MethodHandle methodHandle = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                methodHandle.invoke(this);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        (new MethodHandleGrandFather().new Son()).thinking();
    }
}
3.4:基于栈的字节码解释执行引擎
  1. 解释执行
        
  2. 基于栈的指令集和基于寄存器的指令集
        “1”+“1”的基于栈的指令集如下:
    iconst_1;iconst_1;iadd;istore_0。如果是基于寄存器: mov eax,1
    add eax,1;
  3. 基于栈的解释器执行过程
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值