从原理聊JVM(四):JVM中的方法调用原理

这篇具有很好参考价值的文章主要介绍了从原理聊JVM(四):JVM中的方法调用原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 引言

多态是Java语言极为重要的一个特性,可以说是Java语言动态性的根本,那么线程执行一个方法时到底在内存中经历了什么,JVM又是如何确定方法执行版本的呢?

2 栈帧

JVM中由栈帧存储方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法的调用就是从入栈到出栈到过程。

2.1 局部变量表

局部变量表由变量槽组成,《Java虚拟机规范》指出:“每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据”。

这八种数据类型都可以使用32位或更小的物理内存来存储,如果是64位虚拟机环境下,虚拟机需要通过对齐填充来使变量槽与在32位虚拟机环境下外观一致。

如果是64位的数据类型,比如long和double,JVM会以高位对齐的方式为其分配两个连续的变量槽空间。且规定不允许以任何方式访问这两个变量槽的其中一个,类加载的校验阶段会针对违反规定的行为抛出异常。

类变量会有两次赋值,一次是准备阶段给赋值一个默认值,二是初始化阶段,赋予程序定义的值。但方法变量没有准备阶段,所以没赋值的方法变量不能被使用。

2.2 变量槽的复用

为了节省内存空间,变量槽是可以复用的。当程序计数器的值超过方法体中定义的变量的作用域时,这个变量的变量槽就可以被其他变量复用了。不过虽然这样可以节省内存空间,但对GC有一定影响。

举个例子,如果没有发生即时编译的前提下,在方法清单1中placeholder不会被回收。原因是,方法清单1中gc发生时,变量槽仍然保持着对placeholder的引用,所以不会被标记为可回收对象。而在方法清单2中国呢增加了int a = 0后,placeholder原有的变量槽被变量a复用了,也就不存在引用placeholder的变量槽了,所以placeholder就可以被回收了。

方法清单1:

public static void main (String[] args) {
  {    
    byte[] placeholder = new byte[64 * 1024 * 1024];
  }  
  System.gc();
}

方法清单2:

public static void main (String[] args) {
  {
    byte[] placeholder = new byte[64 * 1024 * 1024];
  }  
  int a = 0;  
  System.gc();
}

但是实际上,大部分程序都是运行在即时编译下的,所以编译器会对其进行优化,实际情况下方法清单1中placeholder也能被回收。

2.3 操作数栈

操作数栈主要作用有二:

1.作为计算过程中的所需变量的临时存储空间

2.存储系统运行过程中的计算中间结果

操作数栈不能通过指针访问,只能通过弹栈和压栈来操作其内部元素。当执行某项指令前会将所需变量压入栈顶,然后真正执行指令时从栈顶依次取出用来执行具体指令,执行完成后会将结果在压入操作数栈。

大多数虚拟机实现会有一些优化处理,将两个栈帧部分重叠:上一个栈帧的部分操作数栈和下一个栈帧的部分局部变量表。不仅节约空间,还让下面栈的操作可以直接使用上面栈的内容,减少了参数传递。

2.4 动态链接

Java文件被编译成Class文件后,变量和方法的引用都作为符号引用保存在Class文件中的常量池中。而对于方法的引用,某些可以在编译期就确定下来称为“直接引用”,而有些方法只能在运行期才能确定下来(比如方法的重载)。

动态链接的作用就是在运行期将符号引用转换为直接引用。

2.5 方法返回地址

一个方法执行完成后,有两种方式退出:正常完成和抛出异常。

当方法A中调用方法B时,A的栈帧中会保存程序计数器的值作为返回地址。而异常退出时,返回地址是要通过异常处理器表来确定的。

方法返回后还会进行几个操作:

1.恢复主调线程对应栈帧中的局部变量表和操作数栈

2.把返回值压入主调线程的栈帧中

3.调整程序计数器到方法调用指令的下一条指令

2.6 附加信息

不同虚拟机在实现时可以自定义一些例如调试、性能收集等信息放到栈帧之中。

3 方法调用

一切方法调用在Class文件里面存储的都只是符号引用,某些调用需要在类加载时甚至运行期间才能确定目标方法的直接引用,这是Java强大的动态扩展能力的基础。

3.1 方法调用指令

JVM共支持以下5种方法调用字节码指令:

•invokestatic调用静态方法

•invokespecial调用构造器()方法、私有方法和父类中的方法

•invokevirtual调用所有虚方法

•invokeinterface调用接口方法,运行期会确定具体实现该接口的对象

•invokedynamic调用运行期动态解析出具体调用的方法

其中,invokestatic和invokespecial指令调用的方法,都可以类加载的解析阶段确定调用的方法版本,Java中符合这个条件的方法共有五种:静态方法、私有方法、实例构造器、父类方法和final修饰的方法(它使用invokevirtual指令调用)。

这五种方法称为“非虚方法”(Non-Virtual Method),剩下的均为“虚方法”(Virtual Method)。

3.2 解析

如果一个方法在类加载的解析阶段就能确定方法的调用版本,那么这类方法的调用被称为解析(Resolution)。

Java中符合解析标准的主要是静态方法私有方法。前者与类型直接相关,后者对外不可见。

方法调用指令中,invokestaticinvokespecial指令调用的方法,再加上final修饰的方法,都被称作“非虚方法”,他们都可以在解析阶段确定唯一的调用版本。其他的方法都被称作“虚方法”。

3.3 分派

在编译阶段,依赖静态类型确定方法的调用版本,这就叫做“静态分派”

而在运行期,根据实际类型确定方法调用版本被称作“动态分派”

3.3.1 静态分派

直接上个🌰:

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 sd = new StaticDispatch();
    Human man = new Man();
    Human woman = new Woman();
    sd.sayHello(man);
    sd.sayHello(woman);
  }
}

输出如下:

hello,guy!
hello,guy!

首先介绍一下静态类型和实际类型。

当声明变量man时:

Human man = new Man();

其中Human被称为“静态类型”,ManWoman叫做“实际类型”。静态类型在编译期可知,实际类型需要等到运行期才能确定。

回到上面的示例,为什么两次输出都是hello,guy!呢?

这是因为编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。所以上面代码示例中,多次不同对象调用sayHello()均找到各自的静态类型对应的方法版本,即sayHello (Human guy)

方法版本的选择顺序

事实上,虽然编译器能确定方法的重载版本,但往往这并不是唯一的,仅仅只能确定一个“相对更合适的“版本。

public class Overload{
  public static void sayHello (Object arg){
  	System.out.println("hello Object");
  }
  public static void sayHello (int arg) {
    System.out.println("hello int");
  }
  public static void sayHello (long arg) {
    System.out.println("hello long");
  }
  public static void sayHello (Character arg){
    System.out.println("hello Character");
  }
  public static void sayHello (char arg) {
    System.out.println("hello char");}
  public static void sayHello (char... arg) {
    System.out.println("hello char...");
  }
  public static void sayHello (Serializable arg) {
    System.out.println("hello Serializable");
  }
  public static void main (String[] args) {
    sayHello('a');
  }
}

输出如下:

hello char

如果删除掉sayHello (char arg)方法,则会匹配到sayHello (int arg)方法,编译器匹配的转型顺序是char > int > long > float > double,但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的。

3.3.2 动态分派

区别于静态分派,如果需要在运行期根据对象类型来确定方法版本,则属于动态分派。

public class DynamicDispatch {
    static abstract class Human {
        public void speak() {
            System.out.println("I'm human");
        }
    }
    static class Man extends Human {
        @Override
        public void speak() {
            System.out.println("I'm man");
        }
    }
    static class Woman extends Human {
        @Override
        public void speak() {
            System.out.println("I'm woman");
        }
    }
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.speak();
        woman.speak();
	      man = new Woman();
        man.speak();
    }
}

输出如下:

I'm man
I'm woman
I'm woman

在上面的例子中,对于speak()方法的调用在编译期完全无法确定,所以只能动态匹配对应的方法版本。那么JVM是如何进行匹配呢?答案就是invokevirtual指令。

invokevirtual指令运行过程

Java做到多态的根本原因是invokevirtual指令的执行逻辑,该命令的运行过程如下:

1.找到操作数栈顶元素的对象的实际类型,记作C。

2.如果在C中找到与常量中描述符和简单名称都一样的方法,则进行访问权限校验,通过则返回方法的直接引用,否则抛出异常java.lang.IllegalAccessError。

3.否则,按照继承关系从下往上依次寻找C类的父类,来进行第二步中的方法查找和校验。

4.最后仍未能找到方法则抛出异常java.lang.AbastractMethodError。

注意:由此也能看出,Java中只有虚方法,没有“虚字段”,如果子类和父类存在相同名称的字段,子类中的字段会覆盖父类中的字段。

3.3.3 静态分派和动态分派的对比

分派类型 原理 发生阶段 应用场景
静态分派 根据静态类型判断方法版本 编译期 重载
动态分派 根据实际类型判断方法版本 运行期 重写

作者:京东科技 康志兴

来源:京东云开发者社区文章来源地址https://www.toymoban.com/news/detail-630341.html

到了这里,关于从原理聊JVM(四):JVM中的方法调用原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Python中的super:调用父类的构造方法

     在子类构造方法中调用父类构造方法的两种方式: 1、未绑定的方法: 直接使用父类名来进行调用 ,即‘父类名.父类方法名(self,[参数1, 参数2,...])’ 2、 super() : 如果涉及多继承,该函数只能调用第一个直接父类的构造方法。super().父类方法名(self, [参数1, 参数2,...])或supe

    2024年02月15日
    浏览(41)
  • 微信小程序之调用子组件中的属性或方法

    不能使用标签选择器,否则返回的是null 在自定义组件中,声明了类名选择器或者是id选择器,在父组件中用this.selectComponent(class或id)去关联 ,就可以使用子组件的属性和方法呢! 父组件的.JS文件中 zujian  class=\\\"名字\\\" id =“名字”  /zujian 然后在父组件的.wxml中的触发函数中

    2024年02月11日
    浏览(24)
  • 在Vue3中,父组件调用子组件中的方法

    前言: 最近在写一个项目的过程中,遇到了父组件需要调用子组件中方法的情况,最终找到了实现方法,总结如下: 1.在子组件中定义方法并暴露出去 2.在父组件中获取子组件并调用子组件中的方法

    2024年02月19日
    浏览(29)
  • 探秘C#中的秘密通道:五种引人注目的方法调用内部或私有方法

    在 C# 中,可以使用不同的方法调用内部或私有方法。下面分别介绍通过反射、MethodInfo.CreateDelegate、表达式(树)、动态方法(call)、动态方法(calli)这五种方法。 使用反射可以访问和调用内部或私有方法。 通过 MethodInfo.CreateDelegate 方法可以创建委托,然后调用私有方法

    2024年02月05日
    浏览(39)
  • Spring事务失效,同一个类中的内部方法调用事务失效

    所谓声明式事务,也就是通过配置的方式,比如通过 配置文件xml或者注解 的方式,来告诉Spring哪些方法需要Spring帮忙管理事务,然后开发者只需要关注业务代码,而事务的事情则由Spring自动帮我们控制。 配置文件的方式 :即在spring.xml文件中进行统一配置,开发者基本不用

    2023年04月18日
    浏览(27)
  • 远程方法调用中间件Dubbo在spring项目中的使用

    作者: 逍遥Sean 简介:一个主修Java的Web网站游戏服务器后端开发者 主页:https://blog.csdn.net/Ureliable 觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言! Dubbo是一个高性能分布式服务的Java RPC框架,它可以可以帮助实现不同应用之间的远程调用

    2024年02月10日
    浏览(28)
  • JavaScript中的事件循环机制,包括事件循环的原理、宏任务和微任务、事件队列和调用栈、以及如何优化事件循环

    JavaScript中的事件循环机制是JavaScript运行引擎的核心之一,它决定了代码的执行方式和效率。本文将从几个方面介绍JavaScript中的事件循环机制,包括事件循环的原理、宏任务和微任务、事件队列和调用栈、以及如何优化事件循环。 一、事件循环的原理 事件循环是JavaScript中实

    2024年02月05日
    浏览(33)
  • 微信小程序中的页面跳转(通过点击按钮、调用方法的形式)

    页面跳转用的多吧、tabBar的空间有限。通过路由的方式跳转时Vue中常用的方式、小程序采用类似的做法。 – 这里通过给按钮绑定点击事件、调用方法、方法中实现页面跳转。(其它方式暂不考虑) 小程序之页面跳转 注意: this.pageRouter.navigateTo 代替 wx.navigateTo 是更优的 log

    2024年02月11日
    浏览(41)
  • 微信小程序关于wxs语法、以及能否引入js中的方法(不能调用)

    页面中使用wxs 或者  参考:WXS | 微信开放文档 wxs能引入外部js文件吗 | 微信开放社区 微信小程序中的WXS语法 1、WXS 中不支持let和const,不支持箭头函数。 2、变量命名必须符合下面两个规则: 首字符必须是:字母(a-zA-Z),下划线(_) 剩余字符可以是:字母(a-zA-Z),下划

    2024年02月06日
    浏览(47)
  • 服务器无法调用gym中的render,采用Monitor保存视频的方法解决

    由于服务器上没有图形化界面,所以在调用gym中的 render() 函数时,会报错 pyglet.canvas.xlib.NoSuchDisplayException: Cannot connect to \\\"None\\\" 。 思路是:把视频保存下来,下载到本地再看。 直接调用 gym.wrappers.Monitor 把视频保存到本地 代码示例如下: outdir 为视频保存路径; video_callable 表

    2023年04月09日
    浏览(23)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包