百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

JVM中方法调用的底层实现,看这一篇就够了

yuyutoo 2024-11-12 14:58 10 浏览 0 评论

方法调用的底层实现

我们编写的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用来调用类的静态方法。

package com.morris.jvm.methodinvoke;

public class InvokeStaticDemo {

    public static void hello() {
    }

    public static void main(String[] args) {
        hello();
    }

}

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

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

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

invokespecial

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

package com.morris.jvm.methodinvoke;

public class InvokeSpecialDemo {

    private void hello() {
    }

    public static void main(String[] args) {
        InvokeSpecialDemo invokeSpecialDemo = new InvokeSpecialDemo();
        invokeSpecialDemo.hello();
    }
}

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

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

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

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

虚方法

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

invokevirtual

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

package com.morris.jvm.methodinvoke;

public class InvokeVirtualDemo {

    public static void main(String[] args) {

        InvokeVirtualDemo invokeVirtualDemo = new InvokeVirtualDemo();
        invokeVirtualDemo.hello();

    }

    public void hello() {
    }
}

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

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

invokeinterface

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

package com.morris.jvm.methodinvoke;

public class InvokeInterfaceDemo {

    public static void main(String[] args) {

        Runnable r = new Runnable() {
            @Override
            public void run() {

            }
        };

        r.run();
    }
}

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

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

lambda表达式

invokedynamic

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

package com.morris.jvm.methodinvoke;

public class InvokeDynamicDemo {

    public static void main(String[] args) {
        Runnable r  = () -> System.out.println("hello");
    }
}

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

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

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

InnerClasses:
     public static final #53= #52 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  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;
    Method arguments:
      #27 ()V
      #28 invokestatic com/morris/jvm/methodinvoke/InvokeDynamicDemo.lambda$main$0:()V
      #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调用方法
package com.morris.jvm.methodinvoke;

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

public class MethodHandleDemo {
    public static void main(String[] args) throws Throwable {
        String hello = toString("hello");
        System.out.println(hello);
        String result = toString(1024);
        System.out.println(result);
    }

    private static String toString(Object  o)  throws  Throwable  {
        //方法类型表示接受的参数和返回类型(第一个参数是返回参数)
        MethodType  methodType  =  MethodType.methodType(String.class);
        //方法句柄--工厂方法Factory
        MethodHandles.Lookup  lookup  =  MethodHandles.lookup();
        //拿到具体的MethodHandle(findVirtual相当于字节码)
        MethodHandle  methodHandle  =  lookup.findVirtual(o.getClass(),  "toString",  methodType);
        String  obj  =  (String) methodHandle.invoke(o);
        return  obj;
    }
}

分派

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

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

静态分派

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

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

package com.morris.jvm.methodinvoke;

public class StaticDispatch{

	static abstract class Human{}
	static class 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,gentleman!");
	}
	public void sayHello(Woman guy){
		System.out.println("hello,lady!");
	}
	public static void main(String[]args){
		StaticDispatch sr = new StaticDispatch();
		Human man = new Man();
		Human woman = new Woman();
		sr.sayHello(man);
		sr.sayHello(woman);

	}
}

运行结果如下:

hello,guy!
hello,guy!

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

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

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

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

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

Human human=new Man();
sr.sayHello((Man)human);
human=new Woman();
sr.sayHello((Woman)human);

运行结果如下:

hello,gentleman!
hello,lady!

动态分派

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

package com.morris.jvm.methodinvoke;

public class Dispatch {
    static class QQ{}
    static class WX{}
    public static class Father{
        public void hardChoice(QQ arg){
            System.out.println("father choose qq");
        }
        public void hardChoice(WX arg){
            System.out.println("father choose weixin");
        }
    }
    public static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("son choose qq");
        }
        public void hardChoice(WX arg){
            System.out.println("son choose weixin");
        }
    }
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new WX());
        son.hardChoice(new QQ());
    }
}

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

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

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

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

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

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

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

相关推荐

java把多张图片导入到PDF文件中(java如果导入图片到项目)

packagecom.mlh.utils;importcom.itextpdf.text.*;importcom.itextpdf.text.Font;importcom.itextp...

聊聊langchain4j的AiServicesAutoConfig

序本文主要研究一下langchain4j-spring-boot-starter的AiServicesAutoConfig...

Spring 中三种 BeanName 生成器!(spring生成bean过程)

无论我们是通过XML文件,还是Java代码,亦或是包扫描的方式去注册Bean,都可以不设置BeanName,而Spring均会为之提供默认的beanName,今天我们就来看看Spr...

Zookeeper实现微服务统一配置中心

Zookeeper介绍本质它是一个分布式服务框架,是ApacheHadoop的一个子项目...

Spring cloud Gateway 动态路由(springboot gateway 动态路由)

一、分析过程...

从Nacos客户端视角来分析一下配置中心实现原理

目录...

Python 中容易被新手忽略的问题(python容易犯的错误)

设置全局变量有时候设置全局变量的需求并不是直接赋值,而是想从某个数据结构里引用生成,可以用下面这两种方法,推荐第二种,golbals()支持字典用法很方便。...

Springboot实现对配置文件中的明文密码加密

我们在SpringBoot项目当中,会把数据库的用户名密码等配置直接放在yaml或者properties文件中,这样维护数据库的密码等敏感信息显然是有一定风险的,如果相关的配置文件被有心之人拿到,必然...

是时候丢掉BeanUtils了(丢掉了时间)

前言为了更好的进行开发和维护,我们都会对程序进行分层设计,例如常见的三层,四层,每层各司其职,相互配合。也随着分层,出现了VO,BO,PO,DTO,每层都会处理自己的数据对象,然后向上传递,这就避免不...

EasyExcel自定义合并单元格多行合并根据自定义字段

第一种方式实现通过定义注解+实现RowWriteHandler接口中的afterRowDispose方法来动态合并行根据指定的key可以是单个字段也可以是多个字段也可以根据注解指定。注解方式使用参考原...

太香了!女朋友熬夜帮我整理的Spring Boot - Banner 笔记,分享给你

上一篇分享的是《Java避坑指南!IDEA查看.class文件源码下载失败问题汇总》,这篇给大家分享《SpringBoot-自定义Banner图案》。...

基于SpringCloud的enum枚举值国际化处理实践

背景选用SpringCloud框架搭建微服务做业务后台应用时,会涉及到大量的业务状态值定义,一般常规做法是:持久层(数据库)存储int类型的值后台系统里用阅读性好一点儿的常量将int类型的值做一层映射...

Lucene就是这么简单(好女婿你以后就是妈妈的老公了)

什么是Lucene??Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,由资深全文检索专家DougCutting所撰写,它是一个全文检索引擎的架构,提供了完整的创建索引和...

注解@Autowired和@Resource的区别总结

零、前言@Autowired和@Resource注解都可以在Spring应用中进行声明式的依赖注入。以前都是看的网上关于两者的区别,但是实际和网上说的有出入,故从源码角度进行分析、验证。...

100个Java工具类之73:系统信息获取工具类SystemUtils

SystemUtils是一个功能强大的工具类。可以获取系统属性、检测java版本、处理跨平台文本文件,合理地使用此类,可以使代码更健壮,系统更安全。...

取消回复欢迎 发表评论: