• 字节码进阶之方法调用指令详解



    在这里插入图片描述

    0. 前言

    Java字节码是Java虚拟机(JVM)执行的代码,它是由Java编译器编译Java源代码生成的。字节码可以在任何安装了JVM的平台上运行,这也是Java“一次编写,到处运行”的基础。

    字节码中,方法调用指令是一类重要的指令,它用于调用Java方法。方法调用指令主要包括五种:invokevirtual、invokespecial、invokestatic、invokeinterface和invokedynamic。每种指令对应一种特定的方法调用场景。

    1. invokevirtual:用于调用非私有实例方法(包括虚方法和超类方法)。
    2. invokespecial:用于调用构造器、私有方法和超类方法。
    3. invokestatic:用于调用静态方法。
    4. invokeinterface:用于调用接口方法。
    5. invokedynamic:用于调用动态方法。

    了解这些方法调用指令的工作机制,可以帮助我们更深入地理解Java方法调用的运行机制,对于深入理解Java语言和虚拟机有很大的帮助。

    章节回顾

    1.《JVM之class文件结构剖析》
    2.《JVM字节码指令详解》
    3.《字节码之常见java语句的底层原理》
    4.《字节码进阶之方法调用指令详解 》
    5.《字节码之Lambda 表达式底层原理 》
    6.《字节码进阶之Lombok底层原理》

    在这里插入图片描述

    1. 五种方法调用指令

    Java虚拟机(JVM)在执行方法调用的过程中,主要涉及到以下五个字节码指令:invokevirtualinvokeinterfaceinvokespecialinvokestaticinvokedynamic。下面将对这五个指令进行详细的解析:

    1. invokevirtual

      invokevirtual指令用于调用对象的实例方法(也就是非私有实例方法,包括接口方法)。这个指令在运行时会在对象的虚方法表中查找到适合的方法进行调用。

    2. invokeinterface

      invokeinterface指令用于调用接口方法。因为一个类可以实现多个接口,所以接口方法的调用需要一个更复杂的查找过程。

    3. invokespecial

      invokespecial指令用于调用构造函数,私有实例方法和父类方法。这个指令在编译时就可以确定具体调用的是哪个方法,所以它不需要在运行时进行查找。

    4. invokestatic

      invokestatic指令用于调用静态方法。由于静态方法不依赖于具体的对象,因此在编译时就可以确定调用的是哪个方法。

    5. invokedynamic

      invokedynamic指令用于实现动态类型语言。这个指令在运行时会动态解析出调用点限定符所引用的方法,并执行该方法。

    以上就是Java的五种方法调用指令的简单介绍。它们的作用和特性各不相同,但是都是为了支持Java丰富的方法调用机制。理解这五个指令能够帮助我们更好地理解Java的运行机制,从而编写出更高效、更稳定的代码。

    2. invokevirtual指令

    2.1 invokevirtual指令的定义和作用

    invokevirtual是Java字节码中的一种方法调用指令,它用于调用对象的非静态方法。这些方法在编译时不能被确定,需要在运行时根据对象的实际类型进行解析。invokevirtual指令执行的步骤包括:首先从运行时栈中弹出该方法的参数,然后再弹出一个对象引用,最后在该对象的类的虚方法表中查找对应的方法并调用。

    2.2 invokevirtual在对象的实例方法调用中的作用

    在Java程序中,我们常常会使用到方法的重写(Override)。当我们通过一个父类引用调用被子类重写的方法时,实际执行的是子类的版本。这是因为虚拟机采用了一种称为动态分派(Dynamic Dispatch)的技术,也就是在运行时根据对象的实际类型确定方法的执行版本。这个过程就用到了invokevirtual指令。

    假设我们有以下一段代码:

    public class Example {
        public void print() {
            System.out.println("Hello, World!");
        }
    }
    
    public class ChildExample extends Example {
        @Override
        public void print() {
            System.out.println("Hello from ChildExample!");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Example example = new ChildExample();
            example.print();  // Output: Hello from ChildExample!
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    虽然example的静态类型是Example,但其实际类型(运行时类型)是ChildExample。因此,虽然example.print()这一行代码在编译时看起来应该调用的是Example类中的print方法,但实际上在运行时调用的是ChildExample中的print方法。这个过程是由invokevirtual指令实现的。

    2.3 分析invokevirtual在运行时如何在虚方法表中查找方法

    JVM的虚方法表(Virtual Method Table)是每个类中用于动态方法查找的一项关键性能。invokevirtual指令的执行过程中,会使用到这个虚方法表。

    当JVM加载类时,会为每个类建立一个虚方法表,其中列出了该类所有的虚方法。虚方法是指在子类中可以被重写的方法,这包括了所有的非私有实例方法,既包括从超类继承的方法,也包括在当前类中声明的方法。

    调用一个实例方法时,如果这个方法在编译时无法确定具体的目标方法(比如在多态情况下),JVM会在运行时动态地通过虚方法表进行查找。

    specifically, 当Java虚拟机执行invokevirtual指令时,首先会从对象头中找到对象的类,然后定位类的虚方法表,并根据方法在虚方法表中的索引进行查找,找到具体的方法后执行。这个过程相对于静态执行过程中的直接调用,会有一定的性能损耗,但是可以支持运行时的多态性。

    例如,我们来思考以下的代码:

    在编译时parent.greet()将调用的是Parent类的greet方法,但在运行时,由于parent引用的实际对象类型是Child,故实际调用的是Child类的greet方法,这个过程就是通过invokevirtual指令和虚方法表完成的。

    public class Parent {
        public void greet() {
            System.out.println("Hello from Parent");
        }
    }
    
    public class Child extends Parent {
        @Override
        public void greet() {
            System.out.println("Hello from Child");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            Parent parent = new Child();
            parent.greet();  // Outputs: "Hello from Child"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    为了提高查找效率,虚方法表在类加载时就已经建立,具有相同签名的方法在表中的位置也是相同的。这使得在运行时,不管对象属于哪个类,只要我们知道了方法在虚方法表中的索引位置,就能快速地定位到具体的方法。

    3. invokeinterface指令

    3.1 invokeinterface指令的定义和作用

    invokeinterface 是 Java字节码指令的一种调用指令,它用于调用接口中的方法。不同于invokevirtual指令,invokeinterface指令是专门用来处理基于接口的多态性。

    invokeinterface指令的作用是,它从运行时栈中弹出该接口方法的参数,然后再弹出一个对象引用,然后在该对象的类的所有实现的接口方法表中查找对应的方法并进行调用。

    由于一个类可以实现多个接口,而这些接口可以有相同的方法,因此在运行时,invokeinterface指令需要在接口方法表中查找具体的方法,这个过程相对于invokevirtual指令的虚方法表查找过程可能会稍微复杂且慢一些。但是,invokeinterface指令支持了Java的接口多态性和动态方法绑定,非常重要。

    3.2 解析invokeinterface在接口方法调用中的作用

    在Java中,接口是一种规范,它规定了实现类应该提供哪些方法。因此,当我们通过接口引用调用这些方法时,需要在运行时确定实际的目标方法。这个过程就是由invokeinterface指令实现的。

    假设我们有以下一段代码:

    public interface Animal {
        void makeSound();
    }
    
    public class Dog implements Animal {
        @Override
        public void makeSound() {
            System.out.println("Dog says: Woof!");
        }
    }
    
    public class Cat implements Animal {
        @Override
        public void makeSound() {
            System.out.println("Cat says: Meow!");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Animal myDog = new Dog();
            Animal myCat = new Cat();
    
            myDog.makeSound();  // Output: Dog says: Woof!
            myCat.makeSound();  // Output: Cat says: Meow!
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在上述代码中,myDog和myCat的静态类型是Animal,但他们的实际类型是Dog和Cat。因此,虽然myDog.makeSound()和myCat.makeSound()这两行代码在编译时看起来应该调用的是Animal接口中的makeSound方法,但实际上在运行时调用的是Dog和Cat类中的makeSound方法。这个过程就是由invokeinterface指令实现的。

    因为一个类可以实现多个接口,而且这些接口中可能有相同的方法,所以invokeinterface指令在运行时需要在接口方法表中进行查找,这个过程相对于invokevirtual指令可能会稍微复杂一些

    3.3 分析invokeinterface在运行时如何查找具体实现的接口方法

    invokeinterface 指令在调用接口方法时,必须在运行期确定出具体的实现此接口的类,然后再转向通过 invokevirtual 的方式进行调用。

    当 Java 虚拟机通过 invokeinterface 指令调用接口方法时,首先会从对象头中找到对象的类,然后根据接口中方法的索引值在类的接口方法表中查找对应的方法。具体的查找过程如下:

    1. 检查对象的类是否实现了要调用的接口。如果没有实现,抛出 IncompatibleClassChangeError。
    2. 如果实现了该接口,JVM会在类的接口方法表中查找该方法的直接引用。
    3. 如果在接口方法表中没有找到对应的方法,那么就会沿着类的继承体系向上查找,直到找到该方法为止。
    4. 如果依然没找到,说明该对象的类没有实现该接口方法,这时会抛出 AbstractMethodError。
    5. 如果找到了对应的方法,然后就会通过 invokevirtual 的方式进行调用。

    举个例子,如果有以下的接口和类:

    interface IA {
        void foo();
    }
    
    class A implements IA {
        public void foo() {
            System.out.println("A.foo");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后在某处的代码中调用:

    IA ia = new A();
    ia.foo();   // Outputs: "A.foo"
    
    • 1
    • 2

    在这个例子中,ia.foo() 的调用就会通过 invokeinterface 指令在运行时确定出具体的调用方法 A.foo()。

    invokeinterface 的这种动态查找过程保证了 Java 中基于接口的多态性。由于一个类可以实现多个接口,而且不同接口可以定义相同的方法,因此 invokeinterface 必须在运行时动态确定具体的调用方法。

    4. invokespecial指令

    4.1 invokespecial指令的定义和作用

    invokespecial指令是Java字节码指令中的一种调用指令。它用于调用一些需要特殊处理的实例方法,包括实例初始化方法(即构造器方法)、私有方法和父类方法。对于这些方法,Java使用invokespecial指令进行调用,以区别于常规的虚方法调用。

    invokespecial指令的主要作用是维持Java的面向对象语义。Java编译器在编译一些特殊的方法调用时,会生成invokespecial指令,以确保这些方法能够得到正确的调用。

    例如,对于实例初始化方法,由于它不是常规的虚方法,不能被重写,因此需要使用invokespecial指令进行调用,以防止动态派发和虚方法调用。对于私有方法,由于它们在子类中不能被访问和重写,因此也需要使用invokespecial指令进行调用,以确保调用的是当前类中定义的方法。对于父类方法,Java允许在子类中通过super关键字来访问,这是一种特殊的调用方式,因此也需要使用invokespecial指令进行调用。

    4.2 解析invokespecial在构造函数、私有实例方法和父类方法的调用中的作用

    1.在构造函数的调用中:

    构造函数是一种特殊的方法,它不能被继承或覆盖。因此,当调用构造函数时,需要使用invokespecial指令来防止动态方法调度。例如:

    public class Dog {
        public Dog() {
            System.out.println("Dog is created");
        }
    
        public static void main(String[] args) {
            new Dog();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这段代码中,当创建Dog对象时,会调用其构造函数。这个调用过程就由invokespecial指令实现。

    2.在私有实例方法的调用中:

    私有实例方法无法被子类覆盖或直接访问,因此调用它们时必须使用invokespecial指令。例如:

    public class Dog {
        private void bark() {
            System.out.println("Dog is barking");
        }
    
        public void makeSound() {
            bark();
        }
    
        public static void main(String[] args) {
            new Dog().makeSound();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这段代码中,makeSound方法调用了私有方法bark,这个调用过程就由invokespecial指令实现。

    3.在父类方法的调用中:

    invokespecial也用于“特殊”调用父类方法的情况。例如,在子类中可以用super关键字调用父类被覆盖的方法。这种情况下,也需要使用invokespecial指令。例如:

    public class Animal {
        void makeSound() {
            System.out.println("The animal makes a sound");
        }
    }
    
    public class Dog extends Animal {
        @Override
        void makeSound() {
            super.makeSound();
            System.out.println("Dog is barking");
        }
    
        public static void main(String[] args) {
            new Dog().makeSound();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这段代码中,Dog类的makeSound方法调用了Animal类的makeSound方法,这个调用过程就由invokespecial指令实现。

    4.3 invokespecial为何在编译时就能确定具体调用的方法

    invokespecial 指令在编译时就能确定具体调用的方法,因为它所调用的方法都是一些特殊的方法,包括构造函数、私有实例方法和父类方法。这些方法具有以下特点:

    1. 构造函数:构造函数是一种特殊的方法,它用于初始化对象。构造函数在类中是唯一的,不能被继承或覆盖。因此,当编译器遇到构造函数调用时,它知道这个调用一定是针对当前类的构造函数,而不是其他类的构造函数。因此,在编译时就可以确定具体调用的构造函数。

    2. 私有实例方法:私有实例方法只能在当前类中被访问,它们不能被子类继承或覆盖。当编译器遇到私有实例方法的调用时,它知道这个调用一定是针对当前类的私有实例方法,而不是其他类的私有实例方法。因此,在编译时就可以确定具体调用的私有实例方法。

    3. 父类方法:当使用 super 关键字调用父类方法时,编译器能清楚地知道调用的是哪个父类的方法。这是因为子类中的 super 引用是在编译时就可以确定的,指向的是它的直接父类。因此,当编译器遇到 super 关键字调用父类方法时,它知道这个调用一定是针对当前类的直接父类的方法。在这种情况下,无论父类方法是否被子类覆盖,都应该调用父类中的原始版本。因此,在编译时就可以确定具体调用的父类方法。

    由于 invokespecial 指令用于调用的方法具有这些特点,编译器在编译时就可以确定具体调用的方法。因此,与 invokevirtual、invokeinterface 这样的动态分派指令相比,invokespecial 指令在编译时具有更高的确定性。

    5. invokestatic指令

    5.1 invokestatic指令的定义和作用

    invokestatic 是 Java 字节码中的一种方法调用指令,它用于调用静态方法。在执行 invokestatic 指令时,编译器会将方法符号引用直接解析为实际的方法调用。

    静态方法是在类级别定义的,它不需要类的实例就可以被调用。因此,静态方法在编译时就可以完全确定,不需要在运行时动态决定。

    invokestatic 指令的主要作用是为了调用静态方法。例如,如果我们有一个类 Math 包含一个静态方法 abs,那么我们可以使用 invokestatic 指令来调用 Math.abs

    public class Test {
        public static void main(String[] args) {
            int result = Math.abs(-10);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这段代码中,调用 Math.abs 方法的字节码指令就是 invokestatic

    5.2 解析invokestatic在静态方法调用中的作用

    invokestatic 在静态方法中的作用主要体现在编译时确定被调用的静态方法,无需在运行时进行多态处理。

    在 Java 中,静态方法是属于类的,而不是属于类的任何一个实例。这意味着静态方法不能被子类重写(可以被隐藏,但是与重写是两个概念),因此无须考虑动态派发的问题。

    例如,以下面的简单程序为例:

    public class HelloWorld {
        public static void sayHello() {
            System.out.println("Hello, World!");
        }
    
        public static void main(String[] args) {
            HelloWorld.sayHello();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这段代码中,main 方法中调用了静态方法 sayHello,这个调用过程就由 invokestatic 指令实现。

    字节码如下:

    public static void main(java.lang.String[]);
    Code:
    0: invokestatic  #2 // Method sayHello:()V
    3: return
    
    • 1
    • 2
    • 3
    • 4

    可以看到,invokestatic 指令明确指出调用的是 sayHello 静态方法,确保了调用的一致性和可靠性。

    5.3 invokestatic为何在编译时就能确定具体调用的方法

    invokestatic在编译时就能确定具体调用的方法,主要是因为它用于调用的是静态方法。

    在Java编程语言中,静态方法是在类级别定义的,而不是在实例级别定义的。这意味着无论你创建了多少个类的实例,静态方法只有一份。因此,静态方法在编译时就可以完全确定,不需要在运行时动态决定。

    另外,静态方法不能被子类重写,因此也不存在需要在运行时通过动态绑定来决定具体调用哪一个方法的问题。

    所以,invokestatic指令在编译时就能确定具体调用的方法。

    6. invokedynamic指令

    在这里插入图片描述

    6.1 invokedynamic指令的定义和作用

    invokedynamic指令是Java 7开始引入的一条新的字节码指令,主要用于实现“动态类型语言”(Dynamically Typed Languages)的方法调用。

    在Java 7之前,Java字节码指令集中的方法调用指令(invokevirtualinvokeinterfaceinvokespecialinvokestatic)在编译时就需要确定调用的具体方法,这在很大程度上限制了Java的动态性。

    invokedynamic指令的引入,使Java能更好地支持动态语言的实现。它允许在运行时动态解析出调用点限定符所引用的方法,并能把方法调用绑定到任意的方法上,提供了更高的灵活性。

    invokedynamic主要通过调用点动态链接(Call Site Dynamic Linking)和类库方法句柄(Libraries Method Handles)两个机制来实现。其中,调用点动态链接允许在运行时确定方法调用,类库方法句柄则提供了对方法、构造函数和字段的轻量级、类型安全的直接访问。

    然而,Java 本身并不是一门动态类型语言,所以在编写 Java 代码时我们并不会直接使用到 invokedynamic 指令。它主要还是为了在 JVM 平台上更好地支持动态语言,如:Groovy、JRuby 等。

    6.2 解析invokedynamic在动态类型语言中的作用

    在Java 7之前,Java虚拟机的方法调用指令只能调用静态类型语言的方法,这对于运行在Java虚拟机上的动态类型语言如Groovy,JRuby等来说是个问题,因为它们的方法在运行时才能决定。为了解决这个问题,Java 7引入了invokedynamic指令。

    invokedynamic指令在字节码层面提供了一种动态绑定方法的机制。它允许开发者在运行时决定方法调用的目标方法。当invokedynamic指令执行时,它会调用一个叫做“引导方法”(Bootstrap Method)的特殊方法,这个引导方法会返回一个“方法句柄”(Method Handle),它指向实际要执行的方法。然后,Java虚拟机就会调用这个方法句柄指向的方法。

    以下是一个使用Java的invokedynamic指令的示例(注意,这是为了示例invokedynamic的工作原理,实际上我们在编写Java代码时是不会直接用到invokedynamic的):

    import java.lang.invoke.*;
    
    public class InvokeDynamicExample {
        public static void testMethod(String arg) {
            System.out.println("testMethod called with arg: " + arg);
        }
    
        public static void main(String[] args) throws Throwable {
            CallSite callSite = bootstrap(MethodHandles.lookup(), "testMethod",
                MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class));
    
            // 获取方法句柄
            MethodHandle methodHandle = callSite.dynamicInvoker();
    
            // 动态调用方法
            methodHandle.invoke("Hello, World!");
        }
    
        public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType callSiteType)
                throws NoSuchMethodException, IllegalAccessException {
            // 找到要调用的方法
            MethodHandle methodHandle = lookup.findStatic(InvokeDynamicExample.class, name,
                MethodType.methodType(void.class, String.class));
    
            return new ConstantCallSite(methodHandle);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在上面的代码中,我们通过引导方法找到了testMethod,并返回了一个指向它的方法句柄,然后我们就能通过invokedynamic动态地调用testMethod了。

    6.3 分析invokedynamic如何在运行时动态解析出调用点限定符所引用的方法

    invokedynamic指令通过引导方法(Bootstrap Method)在运行时动态解析出调用点限定符所引用的方法。

    invokedynamic指令首次执行时,它会调用一个特定的引导方法。这个引导方法会负责创建出一个CallSite对象,这个对象中包含了一个MethodHandle,指向实际要调用的方法。

    引导方法是由开发者提供的,可以用来执行任意的定制解析策略。引导方法的输出是一个CallSite对象,这是一个包含了MethodHandle的对象,能够作为invokedynamic指令的持续调用目标。

    在运行时,引导方法会根据invokedynamic指令的参数和运行环境,动态地解析出需要调用的方法,并构建出对应的MethodHandle。

    一旦引导方法执行完毕,invokedynamic指令就会记住引导方法的结果,也就是CallSite对象。在后续的执行过程中,invokedynamic指令就会直接使用这个CallSite对象,而不需要再次调用引导方法。

    这样,invokedynamic指令就能在运行时动态解析出调用点限定符所引用的方法,并且能够缓存解析结果,从而实现高效的动态方法调用。

    7 参考文档

    https://segmentfault.com/a/1190000040440196

    https://www.baeldung.com/java-invoke-dynamic

    https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5.invokespecial

  • 相关阅读:
    Qt OpenGL(二十二)——Qt OpenGL 核心模式-VAO和VBO
    告诉你,项目管理就用这一页纸
    【机器学习】 逻辑回归算法:原理、精确率、召回率、实例应用(癌症病例预测)
    服务器vs普通电脑
    【17】c++设计模式——>原型模式
    国外LEAD收款渠道介绍:Wise收款教程
    JVM内存和垃圾回收-12.String Table
    Hive与Hbase的区别与联系
    元宇宙iwemeta:风口上的脑机接口,偷偷的解密大脑
    pinia状态管理器使用
  • 原文地址:https://blog.csdn.net/wangshuai6707/article/details/133847901