JVM方法调用过程
JVM方法调用过程
重载和重写
同一个类中,如果出现多个名称相同,并且参数类型相同的方法,将无法通过编译.因此,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这种方法上的联系就是重载.
重载的方法在编译过程中即可完成识别.具体到每一个方法调用,Java编译器会根据所传入参数的声明类型(有别实际类型)来选取重载方法.
选取过程如下:
1.不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
2.如果1中未找到适配的方法,则允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
3.如果2中未找到适配的方法,则在允许自动装拆箱以及可变长参数的情况下选取重载方法.
JVM的静态绑定和动态绑定
Java虚拟机识别方法的关键在于类名/方法名/方法描述符(method descriptor).注:方法描述符由方法的参数类型/返回类型构成.
Java虚拟机中的静态绑定(static binding)指的是在解析时便能够直接识别目标方法的情况;而动态绑定(dynamic binding)则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况.
具体来说,Java字节码中与调用相关的指令共有五种:
1.invokestatic:用于调用静态方法
2.invokespecial:用于调用私有实例方法/构造器,以及使用super关键字调用父类的实例方法/构造器,和所有实现接口的默认方法
3.invokevirtual:用于调用非私有实例方法
4.invokeinterface:用于调用接口方法
5.invokedynamic:用于调用动态方法
示例代码如下:
interface 客户 {
boolean isVIP();
} class 商户 {
public double 折后价格 (double 原价, 客户 某客户) {
return 原价 * 0.8d;
}
} class 奸商 extends 商户 {
@Override
public double 折后价格 (double 原价, 客户 某客户) {
if (某客户.isVIP()) { // invokeinterface
return 原价 * 价格歧视 (); // invokestatic
} else {
return super. 折后价格 (原价, 某客户); // invokespecial
}
}
public static double 价格歧视 () {
// 咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。
return new Random() // invokespecial
.nextDouble() // invokevirtual
+ 0.8d;
}
}
调用指令的符号引用
在编译过程中,目标方法的具体内存地址尚未确定.这时,Java编译器会暂时用符号引用来表示该目标方法.这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符.
符号引用存储在class文件的常量池中.根据目标方法是否为接口方法,又可分为接口符号引用和非接口符号引用.
对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。
1.在 C 中查找符合名字及描述符的方法。
2.如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
3.如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。
从这个解析算法可以看出,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法。
对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找。
1.在 I 中查找符合名字及描述符的方法。
2.如果没有找到,在 Object 类中的公有实例方法中搜索。
3.如果没有找到,则在 I 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致。
经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。
虚方法调用
所有非私有实例方法被调用-->编译-->invokevirtual指令.
接口方法调用-->编译-->invokeinterface指令.
这两种指令,均属于Java虚拟机中的虚方法调用.
多数情况下,Java虚拟机需要根据调用者的动态类型-->确定虚方法调用的目标方法.这个过程被称为动态绑定.相对于静态绑定的非虚方法调用,虚方法调用更加耗时.
在Java虚拟机中,静态绑定包括用于调用静态方法的invokestatic指令,和用于调用构造器/私有实例方法/超类非私有实例方法的invokespecial指令.
如果虚方法调用指向一个标记为final的方法,那么Java虚拟机也可以静态绑定该虚方法调用的目标方法.
Java虚拟机采用了一种用空间换时间的策略来实现动态绑定.它为每个类生成一张方法表,用以快速定位目标方法.
方法表
类加载的准备阶段,除了为静态字段分配内存外,还会构建与该类相关联的方法表.
方法表,时Java虚拟机实现动态绑定的关键所在.
方法表本质上是一个数组,每个数组元素指向一个当前类及其父类中非私有的实例方法.
方法表满足两个特质:
1.子类方法表中包含父类方法表中的所有方法
2.子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同.
pre:方法调用指令中的符号引用会在执行之前解析为实际引用.
静态绑定的方法调用:实际引用-->具体的目标方法
动态绑定的方法调用:实际引用-->方法表的索引值(实际上不止索引值)
在执行过程中,Java虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法--->动态绑定的过程
in fact,使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作 : 访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法.相对于创建并初始化Java栈帧来说,这几个内存解引用操作的开销可以忽略不计.
但是,虚方法调用对性能仍有影响:
方法表的引入带来的优化效果仅存在与解释执行或者即时编译代码的最坏情况下.而且即时编译还拥有两个性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining).
内联缓存
内联缓存是一种加快动态绑定的优化技术.它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法.后续执行中,优先使用缓存,没有则使用基于方法表的动态绑定.
对多态的优化,术语:
1.单态(monomorphic),指的是仅有一种状态的情况
2.多态(polymorphic),指的是有限数量种状态的情况.二态(bimorphic)是多态的其中一种.
3.超多态(megamorphic),指的是更多种状态的情况.通常用某个阈值来区分多态和超多态.
综上,内联缓存对应单态内联缓存/多态内联缓存/超多态内联缓存.
1.单态内联缓存,顾名思义,便是只缓存了一种动态类型以及它所对应的目标方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。
2.多态内联缓存则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法。
注:一般来说,我们会将更加热门的动态类型放在前面。在实践中,大部分的虚方法调用均是单态的,也就是只有一种动态类型。为了节省内存空间,Java 虚拟机只采用单态内联缓存。
在选择内联缓存时,如果未命中则重新使用方法表做动态绑定.这时有两种选择:
1.替换单态内联缓存中的纪录。这种做法就好比 CPU 中的数据缓存,它对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存。因此,在最坏情况下,用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升。
2.劣化为超多态状态。这也是 Java 虚拟机的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。
虽然内联缓存附带内联二字,但是它并没有内联目标方法。这里需要明确的是,任何方法调用除非被内联,否则都会有固定开销。这些开销来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。
JVM处理invokedynamic
在Java中,方法调用会编译为invokestatic/invokespecial/invokevirtual/invokeinterface四种指令.这些类名与包含目标方法类名/方法名/方法描述符的符号引用捆绑.在实际运行之前,Java虚拟机将根据这个符号引用链接到具体的目标方法.
Java7引入了invokedynamic指令,该指令的调用机制抽象出调用点这一概念,并允许应用程序将调用点链接至任何符合条件的方法上.
作为invokedynamic的准备工作,Java7引入了更加底层/更加灵活的方法抽象:方法句柄(MethodHandle).
方法句柄的概念
方法句柄是一种强类型的,能够被直接执行的引用.该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段.当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的getter或者setter方法.
HotSpot虚拟机中方法句柄调用的具体实现 :
以DirectMethodHandle为例,调用方法句柄所使用的invokeExact或者invoke方法具备签名多态性的特性.会根据具体的传入参数来生成方法描述符.其中,invokeExact要求传入的参数和所指向方法的描述符严格匹配.方法句柄还支持增删改参数的操作,这些操作时通过生成另一个充当适配器的方法句柄来实现的.
方法句柄的调用和反射调用一样,都是间接调用.同样都面临无法内联的问题,不过与反射调用不同的是,方法句柄的内联瓶颈在于即时编译器能否将该方法句柄识别为常量.
invokedynamic指令
invokedynamic是Java7引入的一条新指令,用以支持动态语言的方法调用.具体来说,它将调用点(CallSite)抽象成一个Java类,并且将原本由Java虚拟机控制的方法调用以及方法链接暴露给了应用程序.在运行过程中,每一条invokedynamic指令将捆绑一个调用点,并会调用该调用点所链接的方法句柄.
在第一次执行invokedynamic指令时,Java虚拟机会调用该指令所对应的启动方法(BootStrapMethod),来生成调用点,并将之绑定至该invokedynamic指令中.在之后的运行过程中,Java虚拟机则会直接调用绑定的调用点所链接的方法句柄.
在字节码中,启动方法是用方法句柄来指定的.这个方法句柄指向一个返回类型为调用点的静态方法.该方法必须接收三个固定的参数,分别为一个Lookup类实例,一个用来指代目标方法名字的字符串,以及该调用点能够链接的方法句柄的类型.
除了三个必须参数外,启动方法(BootStrapMethod)还可以接收若干个其它的参数,用来辅助生成调用点,或者定位索要链接的目标方法.
Java8的Lambda表达式
在Java8中,Lambda表达式也是借助invokedynamic来实现的
具体来说,Java编译器利用invokedynamic指令来生成实现了函数式接口的适配器.这里的函数式接口指的是仅包括一个非default接口方法的接口,一般通过@FunctionalInterface注解.同时,该invokedynamic指令对应的启动方法将通过ASM生成一个适配器类.
对于没有捕获其它变量的Lambda表达式,该invokedynamic指令始终返回同一个适配器类的实例.对于捕获了其它变量的Lambda表达式,每次执行invokedynamic指令将新建一个适配器类实例.
不管是捕获型的还是未捕获型的Lambda表达式,它们的性能上限皆可以达到直接调用的性能.其中,捕获型Lambda表达式借助了即时编译器的逃逸分析,来避免实际的新建适配器类实例的操作.
JVM方法调用过程的更多相关文章
- Hadoop中客户端和服务器端的方法调用过程
1.Java动态代理实例 Java 动态代理一个简单的demo:(用以对比Hadoop中的动态代理) Hello接口: public interface Hello { void sayHello(S ...
- JVM 方法调用之解析
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还没有涉及到方法内部的具体运行过程.在程序运行时,进行方法调用是最普遍最频繁的操作,但Class文件 ...
- JVM 方法调用之动态分派
1. 动态分派 一个体现是重写(override).下面的代码,运行结果很明显. public class App { public static void main(String[] args) { ...
- Spring杂谈 | 从桥接方法到JVM方法调用
前言 之所以写这么一篇文章是因为在Spring中,经常会出现下面这种代码 // 判断是否是桥接方法,如果是的话就返回这个方法 BridgeMethodResolver.findBridgedMetho ...
- JVM方法调用
当我们站在JVM实现的角度去看方法调用的时候,我们自然会想到一种分类: 1.编译代码的时候就知道是哪个方法,永远不会产生歧义,例如静态方法,private方法,构造方法,super方法. 2.运行时才 ...
- mybatis源码分析(方法调用过程)
十一月月底,宿舍楼失火啦,搞得20多天没有网,目测直到放假也不会来了... 正题 嗯~,其实阅读源码不是为了应付面试,更重要的让你知道,大师是怎样去写代码的,同样是用Java,为啥Clinton Be ...
- go微服务框架go-micro深度学习(四) rpc方法调用过程详解
上一篇帖子go微服务框架go-micro深度学习(三) Registry服务的注册和发现详细解释了go-micro是如何做服务注册和发现在,服务端注册server信息,client获取server的地 ...
- go微服务框架go-micro深度学习 rpc方法调用过程详解
摘要: 上一篇帖子go微服务框架go-micro深度学习(三) Registry服务的注册和发现详细解释了go-micro是如何做服务注册和发现在,服务端注册server信息,client获取serv ...
- JVM 方法调用之静态分派
分派(Dispatch)可能是静态也可能是动态的,根据分派依据的宗量数可分为单分派和多分派.这两种分派方式的两两组合就构成了静态单分派,静态多分派,动态单分派,动态多分派这4种组合.本章讲静态分派. ...
随机推荐
- UVA10256 The Great Divide
怎么又没人写题解,那我来贡献一发好了. 题目意思很简单,平面上有两种颜色的点,问你能否求出一条直线使两种颜色的点完全分开. 首先我们考虑两个点集相离的充要条件,这两个点集的凸包必须相离.(很好证明或者 ...
- C#邮件发送类 简单实用 可自定义发件人名称
上图看效果 MailHelper: public class MailHelper { public bool SendMail(MailSender sender,out string errorM ...
- Maven报错Archive for required library:某.jar' in project '项目名'
Maven报错Archive for required library:某.jar' in project '项目名'cannot be read or is not a valid ZIP file ...
- 前端面试送命题(二)-callback,promise,generator,async-await
前言 本篇文章适合前端架构师,或者进阶的前端开发人员:我在面试vmware前端架构师的时候,被问到关于callback,promise,generator,async-await的问题. 首先我们回顾 ...
- .Net Core 在 Linux-Centos上的部署实战教程(三)
绑定域名,利用Nginx反向代理来操作 1.安装Nginx yun install nginx 安装成功 2.启动nginx service nginx start 报报报错了~~· 运行 ...
- Redux 入门教程(三):React-Redux 的用法
为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux,本文主要介绍它. 这个库是可以选用的.实际项目中,你应该权衡一下,是直接使用 Redux,还是使用 React ...
- hdu1201,hdu6252差分约束系统
差分约束系统一般用来解决a-b>=c的问题,有n个这样的限制条件,求出某个满足这些条件的解 可以将这个问题转化成最长路问题,即b到a的距离最少为c,而有多条b到a的路的话,我们就取最长的b到a的 ...
- 福州大学软件工程1816 | W班 第8次作业[团队作业,随堂小测——校友录]
作业链接 团队作业,随堂小测--校友录 评分细则 本次个人项目分数由两部分组成(博客分满分40分+程序得分满分60分) 博客和程序得分表 评分统计图 千帆竞发图 总结 旅法师:实现了更新,导出,查询, ...
- 【问题解决方案】之 Word 公式编辑器 使用小tips
输入空格:shift+Ctrl+space 换行:直接回车.之后在上方菜单栏中选择"在等号处对齐"
- js 深度复制deepClone
function isObject(obj) { return typeof obj === 'object' && obj != null; } const deepClone =( ...