JVM是如何执行方法调用的?(下)

本文探讨JVM中虚方法调用的性能开销,介绍虚方法调用原理及其实现,包括方法表和内联缓存的概念,以及如何通过方法内联等手段优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设计模式大量使用了虚方法来实现多态,但是虚方法的性能效率并不是很高,因此本篇文章将评估每一种设计模式因为虚方法调用而造成的性能开销。。。

首先要声明的是第一个不应该因为虚方法的性能效率而去放弃良好的设计。第二通常来说,JVM中虚方法调用的性能开销并不大,有些时候甚至可以完全消除。第一个错误是原则上的,第二个错误今天来说一下JVM虚方法调用的具体实现。。。

虚方法调用

上一篇文章提到Java里非私有实例方法调用都会被编译成invokevirtual指令,接口方法调用都会被编译成invokeinterface指令。这都是属于JVM中虚方法调用。。。

大多数情况下,JVM需要根据调用者动态类型来确定虚方法调用的目标方法称之为动态绑定,相对于静态绑定的非虚方法调用来说,虚方法调用更加耗时。。。

JVM静态绑定包括用于调用静态方法的invokestatic指令和用于调用构造器、私有实例方法以及超类非私有实例方法的invokespecial指令。如果虚方法调用指向一个标记为final的方法,那么JVM可以静态绑定该虚方法调用的目标方法。。

JVM虚拟机采取了一种用空间换时间的策略来实现动态绑定,为每个类生成一张方法表,用以快速定位目标方法,那么方法表具体是怎么实现的呢?

方法表

类加载准备阶段除了为静态字段分配内存之外还会构造与该类相关联的方法表。这个数据结构便是JVM实现动态绑定的关键所在,方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。

这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法,方法表满足两个特质:其一子类方法中包含父类方法表中的所有方法;其二子类方法在方法表中的索引值,与它重写的父类方法的索引值相同。

方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用来说,实际引用将指向具体的目标方法,对于动态绑定的方法调用来说,实际引用则是方法表的索引值(实际上并不仅是索引值)。

执行过程中,JVM将获取调用者的实际类型并在该实际类型的虚方法中根据索引值获得目标方法,这个过程便是动态绑定。

实际上使用了方法表的动态绑定与静态绑定相比,仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,相对于创建并初始化Java栈帧来说,这几个内存解引用操作的开销可以不计。

那么是否可以认为虚方法调用对性能没有太大影响呢???

其实是不能的,虽然说优化效果看起来很好,但实际上仅存在于解释执行中或者即时编译代码的最坏情况中。这是因为即时编译还拥有另外两种性能更好的优化手段:内联缓存和方法内联。。

内联缓存

内联缓存是一种加快动态绑定的优化技术,它能缓存虚方法调用中调用者的动态类型以及该类型所对应的目标方法。在之后的执行过程中如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。之后的执行过程中如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到内联缓存则会退化至使用基于方法表的动态绑定。。。

针对多态优化手段中,通常会提及以下三个术语:

  1. 单态指的是仅有一种状态的情况
  2. 多态指的是有限数量种状态的情况,二态是多态的其中一种
  3. 超多态指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下称之为多态,否则称之为超多态

对于内联缓存来说也有对应的单态内联缓存、多态内联缓存和超多态内联缓存。单态内联缓存便是只缓存了一种动态类型以及所对应的目标方法,实现很简单:比较缓存的动态类型,命中直接调用对应的目标方法。

在实践中大部分的虚方法调用均是单态的,就是只有一种动态类型,为了节省空间,JVM只采用单态内联缓存。

当内联缓存没有命中的情况下JVM需要重新使用方发表来进行动态绑定。对于内联缓存中的内容,有两种选择。一是替换单态内联缓存中的记录。这种做法好比CPU中的数据缓存对数据局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者动态类型应保持一致,从而能够有效利用内联缓存。。。

最坏情况下用两种不同类型的调用者轮流执行该方法调用,每次进行方法调用都将替换内联缓存,就是说只有写缓存的额外开销,却没有用缓存的性能提升。

另一种选择就是劣化为超多态状态,也是JVM的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化机会。将直接访问方法表来动态绑定目标方法。与替换内联缓存记录做法相比,牺牲了优化机会但是节省了写缓存的额外开销。

虽然内联缓存附带内联二字,但是并没有内联目标方法,需明确的是任何方法调用除非被内联否则都会有固定开销。开销来源于保存程序在该方法中的执行位置以及新建、压入和弹出新方法所使用的栈帧。。

对于极其简单的方法而言,比如getter/setter,这部分固定开销占据的CPU时间甚至超过了方法本身,此时在即时编译中国方法内联不仅仅能消除方法调用的固定开销,而且还增进了进一步优化的可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值