JVM中方法调用的底层实现

x33g5p2x  于2021-12-18 转载在 其他  
字(7.6k)|赞(0)|评价(0)|浏览(512)

方法调用的底层实现

我们编写的Java代码,经过编译后编程class文件,最后经过类加载器加载后进入了JVM的运行时数据区。

但作为程序员真正关心是代码的执行,代码的执行其实本质上是方法的执行,站在JVM 的角度归根到底还是字节码的执行。

main()方法是JVM指令执行的起点,JVM会创建一个main线程来执行main()方法,以触发JVM 一系列指令的执行,真正地把JVM跑起来。接着,在我们的代码中,就是方法调用方法的过程,所以了解方法在JVM 中的调用是非常必要的。

关于方法的调用,JVM中一共提供了5个字节码指令,来调用不同类型的方法:

  • invokestatic:用于调用类的静态方法。
  • invokespecial:用于调用对象的私有实例方法、构造器this()及父类构造器super()等。
  • invokevirtual:用于调用对象的非私有实例方法,比如被修饰符public和protected修饰的方法,大多数方法调用属于这一种、
  • invokeinterface:和invokevirtual指令类似,只不过这个指令用于调用接口的方法。
  • invokedynamic:用于调用动态方法,常用于lambda表达式。

非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,在类加载的解析阶段,就会把这些方法的符号引用解析为直接引用(即入口地址),这样的方法称为非虚方法。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类构造器4种,再加上被final修饰的方法(尽管它使用 invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用,不需要在方法运行时再去进行动态链接。

invokestatic

invokestatic用来调用类的静态方法。

  1. package com.morris.jvm.methodinvoke;
  2. public class InvokeStaticDemo {
  3. public static void hello() {
  4. }
  5. public static void main(String[] args) {
  6. hello();
  7. }
  8. }

main()方法中会调用静态方法hello(),对应的字节码如下:

  1. 0 invokestatic #2 <com/morris/jvm/methodinvoke/InvokeStaticDemo.hello>
  2. 3 return

这个方法调用在编译期间就明确以常量池项的形式固化在字节码指令的参数之中了。

invokespecial

invokespecial用于调用对象的私有实例方法、构造器this()及父类构造器super()等。

  1. package com.morris.jvm.methodinvoke;
  2. public class InvokeSpecialDemo {
  3. private void hello() {
  4. }
  5. public static void main(String[] args) {
  6. InvokeSpecialDemo invokeSpecialDemo = new InvokeSpecialDemo();
  7. invokeSpecialDemo.hello();
  8. }
  9. }

main()方法创建了一个InvokeSpecialDemo对象,会调用InvokeSpecialDemo的默认构造方法(使用了invokespecial指令),调用私有方法hello()也使用了使用了invokespecial指令:

  1. 0 new #2 <com/morris/jvm/methodinvoke/InvokeSpecialDemo>
  2. 3 dup
  3. 4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeSpecialDemo.<init>>
  4. 7 astore_1
  5. 8 aload_1
  6. 9 invokespecial #4 <com/morris/jvm/methodinvoke/InvokeSpecialDemo.hello>
  7. 12 return

另外在InvokeSpecialDemo的默认构造方法中会调用父类Objcet的构造方法,也使用了invokespecial指令:

  1. 0 aload_0
  2. 1 invokespecial #1 <java/lang/Object.<init>>
  3. 4 return

虚方法

虚方法与非虚方法相反,在类加载时期,无法确定方法最终的调用版本,如果一个方法被重载了,需要根据传入参数的类型才能确定具体调用哪个方法。

invokevirtual

invokevirtual用于调用对象的非私有实例方法,比如被修饰符public和protected修饰的方法,大多数方法调用属于这一种(排除掉被final修饰的方法)。

  1. package com.morris.jvm.methodinvoke;
  2. public class InvokeVirtualDemo {
  3. public static void main(String[] args) {
  4. InvokeVirtualDemo invokeVirtualDemo = new InvokeVirtualDemo();
  5. invokeVirtualDemo.hello();
  6. }
  7. public void hello() {
  8. }
  9. }

main()方法对应的字节码如下:

  1. 0 new #2 <com/morris/jvm/methodinvoke/InvokeVirtualDemo>
  2. 3 dup
  3. 4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeVirtualDemo.<init>>
  4. 7 astore_1
  5. 8 aload_1
  6. 9 invokevirtual #4 <com/morris/jvm/methodinvoke/InvokeVirtualDemo.hello>
  7. 12 return

invokeinterface

invokeinterface和invokevirtual指令类似,只不过这个指令用于调用接口的方法。

  1. package com.morris.jvm.methodinvoke;
  2. public class InvokeInterfaceDemo {
  3. public static void main(String[] args) {
  4. Runnable r = new Runnable() {
  5. @Override
  6. public void run() {
  7. }
  8. };
  9. r.run();
  10. }
  11. }

main()方法对应的字节码如下:

  1. 0 new #2 <com/morris/jvm/methodinvoke/InvokeInterfaceDemo$1>
  2. 3 dup
  3. 4 invokespecial #3 <com/morris/jvm/methodinvoke/InvokeInterfaceDemo$1.<init>>
  4. 7 astore_1
  5. 8 aload_1
  6. 9 invokeinterface #4 <java/lang/Runnable.run> count 1
  7. 14 return

lambda表达式

invokedynamic

invokedynamic用于调用动态方法,常用于lambda表达式。

  1. package com.morris.jvm.methodinvoke;
  2. public class InvokeDynamicDemo {
  3. public static void main(String[] args) {
  4. Runnable r = () -> System.out.println("hello");
  5. }
  6. }

main()方法对应的字节码如下:

  1. 0 invokedynamic #2 <run, BootstrapMethods #0>
  2. 5 astore_1
  3. 6 return

使用lambda表达式在编译时会在类中动态生成一个方法,该方法对应的字节码如下:

  1. InnerClasses:
  2. public static final #53= #52 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
  3. BootstrapMethods:
  4. 0: #26 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  5. Method arguments:
  6. #27 ()V
  7. #28 invokestatic com/morris/jvm/methodinvoke/InvokeDynamicDemo.lambda$main$0:()V
  8. #27 ()V

BootstrapMethods属性在Java1.7以后才有,位于类文件的属性列表中,这个属性用于保存invokedynamic指令引用的引导方法限定符。和上面介绍的四个指令不同,invokedynamic并没有确切的接受对象,取而代之的,是一个叫CallSite的对象。

方法句柄(MethodHandle)

invokedynamic指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的get和set方法,从以下案例中可以看到MethodHandle提供的一些方法。

MethodHandle是什么?简单的说就是方法句柄,通过这个句柄可以调用相应的方法。用MethodHandle调用方法的流程为:

  1. 创建MethodType,获取指定方法的签名(出参和入参)
  2. 在Lookup中查找MethodType的方法句柄MethodHandle,可以通过相应的findxxx方法得到相应的MethodHandle,相当于MethodHandle的工厂方法。
  3. 传入方法参数通过MethodHandle调用方法
  1. package com.morris.jvm.methodinvoke;
  2. import java.lang.invoke.MethodHandle;
  3. import java.lang.invoke.MethodHandles;
  4. import java.lang.invoke.MethodType;
  5. public class MethodHandleDemo {
  6. public static void main(String[] args) throws Throwable {
  7. String hello = toString("hello");
  8. System.out.println(hello);
  9. String result = toString(1024);
  10. System.out.println(result);
  11. }
  12. private static String toString(Object o) throws Throwable {
  13. //方法类型表示接受的参数和返回类型(第一个参数是返回参数)
  14. MethodType methodType = MethodType.methodType(String.class);
  15. //方法句柄--工厂方法Factory
  16. MethodHandles.Lookup lookup = MethodHandles.lookup();
  17. //拿到具体的MethodHandle(findVirtual相当于字节码)
  18. MethodHandle methodHandle = lookup.findVirtual(o.getClass(), "toString", methodType);
  19. String obj = (String) methodHandle.invoke(o);
  20. return obj;
  21. }
  22. }

分派

Java是一门面向对象的程序语言,因为Java具备面向对象的3个基本特征:继承、封装和多态。

分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟机之中是如何实现的?

静态分派

在编译期,根据方法涉及的引用类型(包括参数列表的引用类型和方法的调用者的引用类型)来确定方法调用的(初步)版本,并把相应的符号引用放在字节码指令中,这个步骤叫做静态分派。

先来看一下下面这段代码的执行结果:

  1. package com.morris.jvm.methodinvoke;
  2. public class StaticDispatch{
  3. static abstract class Human{}
  4. static class Man extends Human{ }
  5. static class Woman extends Human{}
  6. public void sayHello(Human guy){
  7. System.out.println("hello,guy!");
  8. }
  9. public void sayHello(Man guy){
  10. System.out.println("hello,gentleman!");
  11. }
  12. public void sayHello(Woman guy){
  13. System.out.println("hello,lady!");
  14. }
  15. public static void main(String[]args){
  16. StaticDispatch sr = new StaticDispatch();
  17. Human man = new Man();
  18. Human woman = new Woman();
  19. sr.sayHello(man);
  20. sr.sayHello(woman);
  21. }
  22. }

运行结果如下:

  1. hello,guy
  2. hello,guy

“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type)。

静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

如果将代码改成下面这样,运行结果就不一样了:

  1. Human human=new Man();
  2. sr.sayHello((Man)human);
  3. human=new Woman();
  4. sr.sayHello((Woman)human);

运行结果如下:

  1. hello,gentleman
  2. hello,lady

动态分派

在运行时,根据对象的实际类型来确定方法的调用版本,这个步骤叫做动态分派。

  1. package com.morris.jvm.methodinvoke;
  2. public class Dispatch {
  3. static class QQ{}
  4. static class WX{}
  5. public static class Father{
  6. public void hardChoice(QQ arg){
  7. System.out.println("father choose qq");
  8. }
  9. public void hardChoice(WX arg){
  10. System.out.println("father choose weixin");
  11. }
  12. }
  13. public static class Son extends Father{
  14. public void hardChoice(QQ arg){
  15. System.out.println("son choose qq");
  16. }
  17. public void hardChoice(WX arg){
  18. System.out.println("son choose weixin");
  19. }
  20. }
  21. public static void main(String[] args) {
  22. Father father = new Father();
  23. Father son = new Son();
  24. father.hardChoice(new WX());
  25. son.hardChoice(new QQ());
  26. }
  27. }

方法的重写也是使用invokevirtual指令,只是这个时候具备多态性。

invokevirtual指令有多态查找的机制,该指令运行时,解析过程如下:

  1. 找到操作数栈顶的第一个元素所指向的对象实际类型,记做c。
  2. 如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError。
  3. 否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常,这就是Java语言中方法重写的本质。

静态链接:在类加载过程中的解析阶段将符号引用转化为直接引用。

动态链接:在方法的运行过程中根据方法的参数类型将符号引用转化为直接引用。

动态分派会在JVM运行时会频繁的、反复的去搜索元数据,所以JVM使用了一种优化手段,就是在方法区中建立一个虚方法表,虚方法表示类信息的一种,在类加载过程中的准备阶段完成初始化,存放在方法区,使用虚方法表索引来替代元数据查找以提高性能。

子类中继承但未重写的方法,在子类的虚方法表中存放的入口地址,就是父类的虚方法表中的入口地址,指向父类的实现。在父子类中,相同符号引用的方法(重写)的方法,其在各自虚方法表中的索引相同。

相关文章