虚拟机字节码执行引擎——方法调用


方法调用并不等同于方法的执行。方法调用的唯一任务就是确定调用的是哪一个方法。一切方法调用在class文件里面存的都是符号引用,而不是方法在内存中的入口地址。

一、解析

解析阶段干什么:

解析阶段是解析在程序运行之前就确定的东西。

解析阶段解析的东西:

私有方法,静态方法这些非虚方法

解析阶段使用到的命令:

  • invokespecial:调用私有方法、父类方法、实例构造器<init>
  • invokestatic:调用静态方法。但不包括final修饰的静态方法(用的是invokespecial)

调用字节码的指令

调用字节码的指令除了上面那两个,还有

  • invokevirtual :调用所有的虚方法 与final修饰的非静态方法(final修饰的非静态方法不是虚方法)。
  • invokeinterface: 调用接口方法。会在运行时确定一个实现此接口的对象
  • invokedynamic:先在运行时动态解析限定符所引用的方法,然后再执行该方法。前面4条指令都是固化在虚拟机内部的,而这条指令的分派逻辑是由用户所设定的引导程序决定的(这里不太明白)。lambda表达式就用到了这条指令。

这里是使用这5个指令的例子:

public class Main {
    public static void sayHello() {
        System.out.println("Hello World!");
    }

    public static final void sayHello2() {
        System.out.println("Hi,world!");
    }

    public final void sayHello3() {
        System.out.println("HaHa!!");
    }

    public static void dynamicMethod(IA ia){
        ia.interfaceA();
    }

    public static void main(String[] args) {
        sayHello();//这里使用的是invokestatic命令
        sayHello2();//这里使用的是invokestatic命令
        Main m = new Main();//调用<init>()是使用invokespecial
        m.sayHello3();//使用的是invokevirtual命令,但是sayHello3是个非虚方法,因为它用final修饰的

        IA ia = new IA() {

            @Override
            public void interfaceA() {
                System.out.println("我是接口方法");
            }
        };
        ia.interfaceA();//这里使用的是invokeinterface命令

        AbstractClass ac=new AbstractClass() {
            @Override
            public void abstractMethod() {
                System.out.println("我是虚方法");
            }
        };
        ac.abstractMethod();//使用的是invokevirtual指令



        dynamicMethod(()->{ //使用lambda表达式时使用的是invokedynamic指令,调用dynamicMethod任然使用的是invokestatic指令
            System.out.println("lambda表达式子");
        });

        IA temp=new IA(){//调用IA的实例构造器使用的是invokespecial指令

            @Override
            public void interfaceA() {
                System.out.println("没有使用lambda表达式");
            }
        };

        dynamicMethod(temp);//调用dynamicMethod任然使用的是invokestatic指令


    }
}

interface IA {
    void interfaceA();
}

abstract class AbstractClass {
    public abstract void abstractMethod();
}

class SubClassA extends AbstractClass{

    @Override
    public void abstractMethod() {
        System.out.println("SubClassA重写了父类方法");
    }
}


class SubClassB extends AbstractClass {

    @Override
    public void abstractMethod() {
        System.out.println("SubClassB重写了父类方法");
    }
}

运行结果:
在这里插入图片描述

然后javap -verbose Main,得到结果如下(我只截了main方法的):

在这里插入图片描述

虚方法、非虚方法:

  • 非虚方法:包括静态方法、私有方法。《java虚拟机规范》明确规定了final修饰的方法也为非虚方法。
  • 虚方法:不是非虚方法的方法。

二、分派

按分派调用的方式可以分为静态和动态方式。按分派的宗量可分为单分派和多分派。

2.1 静态分派

首先来一个例子,看一下输出:

public class Main {
    public void sayHello(AbstractClass ac) {
        System.out.println("Hello World!");
    }

    public void sayHello(SubClassA a) {
        System.out.println("SubClassA say hello");
    }

    public void sayHello(SubClassB b) {
        System.out.println("SubClassB say hello");
    }
    public static void main(String[] args) {
        Main m = new Main();
        AbstractClass ac1 = new SubClassA();//静态类型是AbstractClass,实际类型是SubClassA
        AbstractClass ac2 = new SubClassB();//静态类型是AbstractClass,实际类型是SubClassB
        m.sayHello(ac1);//调用sayyHello方法时使用的是invokevirtual指令
        m.sayHello(ac2);
    }
}

abstract class AbstractClass {
    public abstract void abstractMethod();
}

class SubClassA extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassA重写了父类方法");
    }
}

class SubClassB extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassB重写了父类方法");
    }
}

输出结果是:
在这里插入图片描述

静态类型和实际类型:

AbstractClass ac1 = new SubClassA();//静态类型是AbstractClass,实际类型是SubClassA

静态类型和实际类型的区别:
首先我们看静态类型变化和实际类型变化

AbstractClass ac=new SubClassA();
ac=new SubClassB();//实际类型变化

m.sayHello((SubClassA) ac);//静态类型变化,ac本身的静态类型是没有变化的
m.sayHello((SubClassB) ac);//静态类型变化,ac本身的静态类型是没有变化的
  • 静态类型是在编译期就确定的,而实际类型是在运行期确定的。

重载是静态分派的典型应用。重载往往是找出最合适的匹配方法。重载方法的匹配顺序。
byte–>short–>int–>long–>float–>double–>对应的装箱类–>(现在可以上转型了)

例子:

import java.io.Serializable;
public class Main {
    public void doSomething(char c) {
        System.out.println("char c");
    }

    public void doSomething(int c) {
        System.out.println("int c");
    }

    public void doSomething(long c) {
        System.out.println("long c");
    }


    public void doSomething(float c) {
        System.out.println("float c");
    }


    public void doSomething(double c) {
        System.out.println("doulbe c");
    }


    public void doSomething(Double c) {
        System.out.println("Double c");
    }


    public void doSomething(Object c) {
        System.out.println("Object c");
    }


    public void doSomething(Character c) {
        System.out.println("Character c");
    }


    public void doSomething(Serializable c) {
        System.out.println("Serializable c");
    }


    public static void main(String[] args) {
        Main m = new Main();
        char c = 3;
        m.doSomething(c);

    }
}

2.2 动态分派

用一个例子来解释动态分派:

public class Main {
    public static void main(String[] args) {
        Main m = new Main();
        AbstractClass a=new SubClassA();
        AbstractClass b=new SubClassB();
        a.abstractMethod();//解析后的指令都为Method AbstractClass.abstractMethod:()V
        b.abstractMethod();//解析后的指令都为Method AbstractClass.abstractMethod:()V

    }
}

class AbstractClass {
    public void abstractMethod() {
    }
}

class SubClassA extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassA重写了父类方法");
    }
}

class SubClassB extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassB重写了父类方法");
    }
}

javap -verbose Main后的结果(注意25行和29行):
在这里插入图片描述
运行结果:
在这里插入图片描述
问题来了,既然解析是的指令都一样,为什么运行后的结果不一样呢??在说明为什么之前,我们先知道个概念:调用方法的对象称为接受者

invokevirtual运行时的解析过程

  • 找到操作数栈顶的第一个元素的实际类型,记做C。
  • 如果在类型C中找到与常量中的描述符和简单名词相符的方法,则进行访问权限校验,如果通过就返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError。
  • 否则,按照继承关系从下往上依次进行第2个步奏。
  • 如果始终没找到合适的 方法,则抛出java.lang.AbstractMethodError。

2.3 单分派与多分派

方法的接受者与方法的参数统称为方法的宗量。

public class Main {
    public static void main(String[] args) {
        Father father=new Father();
        Father son=new Son();
        father.hardChoice(new _360());//Method Father.hardChoice:(L_360;)V
        son.hardChoice(new QQ());// Method Father.hardChoice:(LQQ;)V
    }
}

class QQ {

}

class _360 {
}

class Father{
    public void hardChoice(QQ arg){
        System.out.println("father choose qq");
    }
    public void hardChoice(_360 arg){
        System.out.println("father choose 360");
    }
}

class Son extends Father{
    public void hardChoice(QQ arg){
        System.out.println("son choose qq");
    }
    public void hardChoice(_360 arg){
        System.out.println("son choose 360");
    }
}

javap -verbose Main之后(注意24和35):
在这里插入图片描述

静态解析时依据的是静态类型参数,运行时是依据接受者的实际类型.

目前(直至java1.8)为止java语言是一门静态多分派动态单分派的语言。

2.4 虚拟机动态分派的实现

每个虚拟机对怎样实现动态分派是有所区别的。动态分派是一个频繁的动作。基于性能的考虑,最常用的“稳定手段”是在类的方法区中建立虚方法表(与之对应在invokeinterface时也会用到接口方法表)。

比如上面代码的虚方法表结构如下:
在这里插入图片描述

虚方法表存的是各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么父类和子类虚方法表中该方法的入口地址一样

还要注意的是具有相同签名的方法,在子类和父类的虚方法表中都应当具有一样的索引序号

三、动态语言支持

3.1 动态语言类型

什么是动态语言类型

类型检查的主体在程序运行期间,而不是编译期间。满足这个特征的语言有JavaScript、PHP、Clojure、Groovy、Jython、Python、Ruby。与之相反的就是静态类型语言,如Java、C++。

ECMAScripte动态类型语言与java等静态类型语言的区别

一句话:“变量无类型而变量值才有类型”

动态类型语言和静态类型语言各自的优缺点

静态类型语言最显著的好处就是提供了严谨的类型检查,这样与类型相关的问题在编码期间就能及时发现,利于稳定性及代码达到更大规模。动态类型语言在运行期间确定类型,这给开发人员提供了更大的灵活性,在某些静态语言需要大量代码来实现的功能,由动态语言编写会更加清晰和简洁,意味着开发效率的提升。

3.2 MethodHandle

MethodHandle是一种动态确定方法的机制。和C++中的方法指针类似。

例子:

import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别

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

public class Main {
    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();
        //调用方法
        getPrintlnMH(obj).invokeExact("fengli");
    }

    /**
     * @param reveiver 方法的接受者
     * @return
     */
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        //方法的返回类型为void,方法的参数为一个String类型的参数
        MethodType mt = MethodType.methodType(void.class, String.class);
        //在reveiver类型中查找叫做println的mt类型的方法。
        return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }
}


上面的功能也可以用反射完成。那么MethodHandle和反射的区别:

  • 最大的区别是MethodHandle的设计是为了服务于所有java虚拟机之上的语言,包括Java语言。而反射值服务于java语言
  • 反射和方法句柄都是模拟方法调用,反射是在java代码层次的模拟,而MethodHandle是在字节码层次的模拟。MethodHandles.lookup中的三个方法findStatic()、findVirtual()、findSpecial分别对应于字节码层面的invokestatic、invokevirtual、invokespecial。
  • Reflection中的java.lang.reflect.Method对象包含的信息远比MethodHandle机制中的java.lang.invoke.MethodHandle多。也就是反射是重量级的 ,而MethodHandle是轻量级的。

3.3 invokedynamic指令

invokedynamic和方法句柄一样都是为了解决——如何把查找目标方法的决定权交给用户问题。MethodHandle和invokedynamic指令的区别:MethodHandle是用上层java代码和API实现的,而invokedynamci是用字节码和Class中其他属性、常量来实现的

invokedynamic指令的介绍

含有invokedynamic的地方称为动态调用点,这个指令的第一个参数是CONSTANT_InvokeDynamic_info常量(包含引导方法、方法类型、名称)。

例子:

import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别

import java.lang.invoke.*;

public class Main {


    public static void main(String[] args) throws Throwable {
      INDY_BootstrapMethod().invokeExact("fengli");
    }

    public static void testMethod(String s){
        System.out.println("hello String:"+s);
    }

    /**
     * 引导方法
     * @param lookup
     * @param name
     * @param mt
     * @return 表示真正要执行的目标方法调用
     * @throws Throwable
     */
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup,String name, MethodType mt) throws Throwable{
        return new ConstantCallSite(lookup.findStatic(Main.class,name,mt));
    }

    private static MethodType MT_BootstrapMethod(){
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",null);
    }

    private static  MethodHandle MH_BootstrapMethod() throws Throwable{
        return lookup().findStatic(Main.class,"BootstrapMethod",MT_BootstrapMethod());
    }

    private static  MethodHandle INDY_BootstrapMethod() throws Throwable{
        CallSite cs=(CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(),"testMethod",MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V",null));
        return cs.dynamicInvoker();
    }
}


首先,我们要知道仅依靠java语言的编译器javac是没有办法生成带有invokedynamic指令的字节码的(lambda表达式例外)。我们需要使用[INDY]将字节码转换为我们最终所要的字节码。

3.4 方法分派的例子

invokedynamic与其他4条invoke指令最大的差别就是它的分派逻辑并不是虚拟机决定的,而是有程序员决定的。
例子,获取族类的方法:

import static java.lang.invoke.MethodHandles.lookup;//这和其他的import有什么区别

import java.lang.invoke.*;
import java.lang.reflect.Field;

public class Main {
    class GrandFather {
       public void thinking() {
            System.out.println("i am grandfather");
        }
    }

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

    class Son extends Father {
       public void thinking() {
            try {
                //jdk1.7
               /* MethodType mt = MethodType.methodType(void.class);
                MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                mh.invoke(this);*/
                //jdk1.8
                MethodType mt = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                MethodHandles.Lookup lkp = (MethodHandles.Lookup)IMPL_LOOKUP.get(null);
                MethodHandle h1 = lkp.findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
                h1.invoke(this);

            } catch (Throwable e) {
                System.out.println("error");
            }
        }
    }

    public static void main(String[] args) throws Throwable {
        (new Main()).new Son().thinking();
    }


}

四、基于栈的字节码解释执行引擎

什么叫做解释执行、编译执行?

解释执行边解释边执行;而编译执行就是一次性将程序源码编译称目标代码,然后执行。

解释执行、编译执行的过程(从抽象语法树开始,上面那条分支是解释执行的,下面那条是编译执行的):
在这里插入图片描述

指令集分为基于栈的指令集和基于寄存器的指令集。下面用一个例子说明他俩。例子,计算“1+1”。
基于栈的指令集是这样的:
iconst_1
iconst_1
iadd
istore_0
基于寄存器的指令集是这样的:
mov eax,1
add eax,1

基于栈的指令集和基于寄存器的指令集的优缺点:

指令集优点缺点
基于栈的指令集可移植,代码相对紧凑,编译器实现更加简单执行速度相对慢一些
基于寄存器的指令集执行速度快可移植性差

基于栈的解释器执行过程:

public class Main {
    public static void main(String[] args){
      System.out.println(new Main().calc());
    }
    public  int calc(){
        int a=100;//java虚拟机会自动选择最合适的指令,比如这里是sipush
        int b=200;
        int c=300;
        return (a+b)*c;
    }
}

javap -v -p后:
在这里插入图片描述

字节码执行情况图:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
需要注意的是实际虚拟机实现可能会和概念模型有很大差距。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java虚拟机可以读取字节码文件并将其转换成可执行的代码。字节码文件是Java源代码编译后生成的二进制文件,它包含了一系列指令,这些指令被Java虚拟机解释和执行。通过这种方式,Java程序可以在不同的硬件平台和操作系统上运行,实现了"Write Once, Run Anywhere"的目标。 Java虚拟机读取字节码文件的过程可以简单概括为以下几个步骤: 1. 加载:Java虚拟机通过类加载器加载字节码文件,将其转换为运行时的类对象。类加载器负责查找并加载类文件,并将其转换为内存中的类对象。 2. 验证:在加载字节码文件后,Java虚拟机会对字节码文件进行验证,确保其符合Java语言规范和虚拟机规范。验证过程包括对字节码文件的结构、语义和安全性进行检查。 3. 准备:在验证通过后,Java虚拟机会为类变量(静态变量)分配内存,并设置默认初始值。此时,还没有执行任何Java代码。 4. 解析:在准备阶段之后,Java虚拟机会对字节码文件中的符号引用进行解析,将其转换为直接引用。这个过程将类或接口的符号引用解析为实际的内存地址。 5. 初始化:在准备阶段之后,Java虚拟机执行类的初始化操作,包括执行静态初始化块和静态变量的赋值操作。在这个阶段,Java程序的主方法会被调用,程序开始执行。 通过以上步骤,Java虚拟机可以读取字节码文件并执行其中的指令,实现Java程序的运行。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java 进阶:实例详解 Java 虚拟机字节码指令](https://blog.csdn.net/m0_54853420/article/details/126104672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值