2025-12-23 库洛一面(最炸裂的一面)
一面
全底层拷打,一点实习不问,游戏厂业务不对口所以直接问底层
如何理解多态
多态的底层原理是什么
多态具体是如何实现的
多态是如何区分接口的方法和父类子类的方法,从底层来说
@Override是一个表现,不是底层,我要你说这个更深层次区分的底层
如何理解接口?棱形继承?
接口里除了定义抽象方法还能定义什么方法
integer a = 200, integer b = 200,比较结果是True还是false。那要是值都是15呢?
异常和错误的继承关系
JVM 的内存模型
程序计数器是什么?
程序计数器的底层是什么知道吗?我要你说的比刚刚更具体一点
Redisson的看门狗机制说一下
Redis在集群中作为分布式锁,然后它假如说某个服务器宕机了,它这个锁会有什么问题,然后要怎么去通过什么方式去解决的
PS:这个就是简单的强一致性锁对比,也就是红锁以及Redis主从切换宕机出现的锁丢失问题,然后对比一下Zookeeper就好了
参考文章:https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg
Redis 当中那个 zset 它的一个作用是什么,一般会用来干什么
说一些跳表
跳表的查找流程
Redis持久化的方式
那你知道 Redis 它的一个就分布式集群当中,它这个数据它是怎么均匀的写入到各个不同的节点
MySQL 当中它索引的一个设计的一些策略或者原则吧
B+树对比B树的优缺点
可重复读能通过什么去解决
索引失效场景
介绍一下NIO
Epoll,同步IO,Reactor模型,Socket,NIO中的三个角色
TCP 它的半连接和全连接分别发生在哪个阶段?
全连接攻击是一个怎么样的现象?
全连接攻击怎么防护?
PS:这个我说了基本的僵尸网路和恶意IP和恶意连接打满Accept队列顺带讲了一下半连接攻击,但是面试官说我的方法在业务层面上,这种处理方案是建立了连接后才判断这个连接是否是恶意连接,连接是否有响应,然后黑白名单中台,基本的IP拦截那些。但是面试官要求的是,【我要的是在你建立全连接之前就实现各种判断和防护,你知道这种在建立全连接之前就拦截处理而不是建立全连接后才拦截处理,这种是怎么在底层实现的】
说一下你对协程的理解
它能否利用多核性能?
虚拟线程它是否具备可抢占性呢?是否能被抢占
虚拟线程的话是如何进行切换的?让你来切换的话你会怎么做?
算法:
相交链表
图搜BFS(后面力扣找到相同的图论题:1091二进制矩阵中的最短路径)
从图的中间开始向周围扩散,bfs搜索,你该怎么搜,说思路,或者写伪代码
我不要树的BFS,我要图的BSF......
场景题:当我们 Linux 操作系统它 CPU 飘高的时候 我们要如何去定位当前操作系统上的一个问题?
top命令之后你是如何进入到具体的进程和线程查看的?我要具体的命令
进程间的通信方式说一下
评价一下面试
我的八股呢其实是公众号+JavaGuide+小林coding+王二Java全部看完了甚至过了10几轮,但是呢其实这些博客有自己的局限,就是他们只能说到Java浅层的且不同博客有自己的风格并不能全面学习整个Java的体系,例如他们的一些JVM和JUC其实也是算表层的东西并没有真的深入,毕竟只是一个博客,覆盖的是常规面试题并不是真的底层
因为学习网站的缺失,弄得我学过的就是会,没学过的就是不会,我想进一步补全一下我的知识体系却苦于找不到这种优质博主
后面为了更加进一步学习这种底层我就在找代替这几个博客的且可以更加深入的学习网站,耗费了好几天终于找到了极客时间
极客时间的话讲底层会比上面的博客更深入,上面的博客只是常规面试题,要是更底层的东西就要嗑极客时间
例如一些方法内联,JNI,静态绑定(重写)和动态绑定(重载)的更底层这种,在javaguide,王二,小林里面都不会说的那么深,以及一些异地多活还有基本的DDD架构设计,也是部分公众号和极客时间讲的更深入一点
被库洛面到这种更底层的问题后我就一直在找这种相关文章,终于在极客时间里面找到了
然后我还会把一些其他博客没有但是极客时间有的东西,且在面试中有价值可以拓展的东西就记到我自己的笔记里面,例如这些
这也算是这次面试给我带来的好处,虽然拷打我拷打得很难受,但是却让我找到一些更底层的东西以及其他补全了我知识体系
如果懒得打开网站自己看和整理的话,可以看我整理的缩略版
其实主要就是:
- 方法表是如何实现动态绑定的(重载的底层)
- 静态绑定是如何区分和实现的(父子类重写的底层)
- 内联缓存(如何优化虚方法表绑定方法的流程和性能)
这个是从我笔记复制过来的,因为牛客会把笔记中的高亮和颜色部分还原成默认和黑色,所以凑合着看吧
如果嫌麻烦就直接跳到虚方法总结那一栏,毕竟校招生面试的时候只要你知道这种东西应该就不会难为你了,所以不用了解的那么全,了解个大概应该够了,我记得虎牙的面试题有个也是问【如何区分静态绑定和动态绑定】的
最后 程序计数器的底层是什么都问出来了我只能说What can I say,曼巴Out了
Java多态的底层原理
重载与重写
如何判定为重载
在Java程序里,如果同一个类中出现多个名字相同,并且参数类型相同的方法,那么它无法通过编译
也就是说,在正常情况下,如果我们想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,我们称之为重载
小知识:这个限制可以通过字节码工具绕开。也就是说,在编译完成之后,我们可以再向class文件中添加方法名和参数类型相同,而返回类型不同的方法。当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在Java编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?当前版本的Java编译器会直接选取第一个方法名以及参数类型匹配的方法。并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。
重载的方法在编译过程中即可完成识别
具体到每一个方法调用,Java编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法
选取的过程共分为三个阶段:
1. 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
2. 如果在第1个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
3. 如果在第2个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
如果Java编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系
在开头的例子中,当传入null时,它既可以匹配第一个方法中声明为Object的形式参数,也可以匹配第二个方法中声明为String的形式参数
由于String是Object的子类,因此Java编译器会认为第二个方法更为贴切
除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法
也就是说,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载
如何判定为重写
那么,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同,那么这两个方法之间又是什么关系呢?
1. 如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法
2. 如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法
众所周知,Java是一门面向对象的编程语言,它的一个重要特性便是多态
而方法重写,正是多态最重要的一种体现方式:它允许子类在继承父类部分功能的同时,拥有自己独特的行为
打个比方,如果你经常漫游,那么你可能知道,拨打10086会根据你当前所在地,连接到当地的客服
重写调用也是如此:它会根据调用者的动态类型,来选取实际的目标方法
JVM的静态绑定与动态绑定
静态绑定(重写)
接下来,我们来看看Java虚拟机是怎么识别方法的。
Java虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor)
至于方法描述符,它是由方法的参数类型以及返回类型所构成
在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么Java虚拟机会在类的验证阶段报错
可以看到,Java虚拟机与Java语言不同,它并不限制名字与参数类型相同,但返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说
由于字节码所附带的方法描述符包含了返回类型,因此Java虚拟机能够准确地识别目标方法
Java虚拟机中关于方法重写的判定同样基于方法描述符
也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java虚拟机才会判定为重写
对于Java语言中重写而Java虚拟机中非重写的情况,编译器会通过生成桥接方法[2]来实现Java中的重写语义。
由于对重载方法的区分在编译阶段已经完成,我们可以认为Java虚拟机不存在重载这一概念
因此在某些文章中
重载也被称为静态绑定(static binding),或者编译时多态(compile-time polymorphism)
重写则被称为动态绑定(dynamic binding)
这个说法在Java虚拟机语境下并非完全正确
这是因为某个类中的重载方法可能被它的子类所重写
因此Java编译器会将所有对非私有实例方法的调用编译为需要动态绑定的类型
确切地说
Java虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况
动态绑定(重载)
而动态绑定则指的是需要在运行过程中根据【调用者的动态类型】来识别目标方法的情况
具体来说,Java字节码中与调用相关的指令共有五种
1. invokestatic:用于调用静态方法
2. invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法
3. invokevirtual:用于调用非私有实例方法
4. invokeinterface:用于调用接口方法
5. invokedynamic:用于调用动态方法
由于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;
}
}
在代码中,“商户”类定义了一个成员方法,叫做“折后价格”,它将接收一个double类型的参数,以及一个“客户”类型的参数。这里“客户”是一个接口,它定义了一个接口方法,叫“isVIP”
我们还定义了另一个叫做“奸商”的类,它继承了“商户”类,并且重写了“折后价格”这个方法。如果客户是VIP,那么它会被给到一个更低的折扣
在这个方法中,我们首先会调用“客户”接口的”isVIP“方法。该调用会被编译为invokeinterface指令。
如果客户是VIP,那么我们会调用奸商类的一个名叫“价格歧视”的静态方法。该调用会被编译为invokestatic指令。如果客户不是VIP,那么我们会通过super关键字调用父类的“折后价格”方法。该调用会被编译为invokespecial指令
在静态方法“价格歧视”中,我们会调用Random类的构造器。该调用会被编译为invokespecial指令。然后我们会以这个新建的Random对象为调用者,调用Random类中的nextDouble方法。该调用会被编译为invokevirutal指令
对于invokestatic以及invokespecial而言,Java虚拟机能够直接识别具体的目标方法
而对于invokevirtual以及invokeinterface而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法
唯一的例外在于,如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为final[3][4],那么它可以不通过动态类型,直接确定目标方法
调用指令的符号引用
在编译过程中,我们并不知道目标方法的具体内存地址
因此Java编译器会暂时用符号引用来表示该目标方法
这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符
符号引用存储在class文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。我在文章中贴了一个例子,利用“javap -v”打印某个类的常量池,如果你感兴趣的话可以到文章中查看
// 在奸商.class的常量池中,#16为接口符号引用,指向接口方法"客户.isVIP()"。而#22为非接口符号引用,指向静态方法"奸商.价格歧视()"。
$ javap -v 奸商.class ...
Constant pool:
...
#16 = InterfaceMethodref #27.#29 // 客户.isVIP:()Z
...
#22 = Methodref #1.#33 // 奸商.价格歧视:()D
...
上一篇中我曾提到过,在执行使用了符号引用的字节码前,Java虚拟机需要解析这些符号引用,并替换为实际引用
对于非接口符号引用,假定该符号引用所指向的类为C,则Java虚拟机会按照如下步骤进行查找
1. 在C中查找符合名字及描述符的方法
2. 如果没有找到,在C的父类中继续搜索,直至Object类
3. 如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足C与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个
从这个解析算法可以看出,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法
对于接口符号引用,假定该符号引用所指向的接口为I,则Java虚拟机会按照如下步骤进行查找
1. 在I中查找符合名字及描述符的方法
2. 如果没有找到,在Object类中的公有实例方法中搜索
3. 如果没有找到,则在I的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤3的要求一致
经过上述的解析步骤之后,符号引用会被解析成实际引用
对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针
小例子
我们知道,设计模式大量使用了虚方法来实现多态
但是虚方法的性能效率并不高,所以我就说,是否能够在此基础上写篇文章,评估每一种设计模式因为虚方法调用而造成的性能开销,并且在文章中强烈谴责一下
当时呢,我老板教的是一门高级程序设计的课,其中有好几节课刚好在讲设计模式的各种好处。所以,我说完这个Idea,就看到老板的神色略有不悦了,脸上写满了“小郑啊,你这是舍本逐末啊”,于是,我就连忙挽尊,说我是开玩笑的
在这里呢,我犯的错误其实有两个。第一,我不应该因为虚方法的性能效率,而放弃良好的设计。第二,通常来说,Java虚拟机中虚方法调用的性能开销并不大,有些时候甚至可以完全消除。第一个错误是原则上的,这里就不展开了。至于第二个错误,我们今天便来聊一聊Java虚拟机中虚方法调用的具体实现
首先,我们来看一个模拟出国边检的小例子
abstract class Passenger {
abstract void passThroughImmigration();
@Override
public String toString() { ... }
}
class ForeignerPassenger extends Passenger {
@Override
void passThroughImmigration() { /* 进外国人通道 */ }
}
class ChinesePassenger extends Passenger {
@Override
void passThroughImmigration() { /* 进中国人通道 */ }
void visitDutyFreeShops() { /* 逛免税店 */ }
}
Passenger passenger = ...
passenger.passThroughImmigration();
这里我定义了一个抽象类,叫做Passenger,这个类中有一个名为passThroughImmigration的抽象方法,以及重写自Object类的toString方法
然后,我将Passenger粗暴地分为两种:ChinesePassenger和ForeignerPassenger
两个类分别实现了passThroughImmigration这个方法,具体来说,就是中国人走中国人通道,外国人走外国人通道。由于咱们储蓄较多,所以我在ChinesePassenger这个类中,还特意添加了一个叫做visitDutyFreeShops的方法
那么在实际运行过程中,Java虚拟机是如何高效地确定每个Passenger实例应该去哪条通道的呢?我们一起来看一下
虚方法调用
在上一篇中我曾经提到,Java里所有非私有实例方法调用都会被编译成invokevirtual指令,而接口方法调用都会被编译成invokeinterface指令
这两种指令,均属于Java虚拟机中的虚方法调用
在绝大多数情况下,Java虚拟机需要根据调用者的动态类型来确定虚方法调用的目标方法
这个过程我们称之为动态绑定
那么,相对于静态绑定的非虚方法调用来说,虚方法调用更加耗时
在Java虚拟机中,静态绑定包括用于调用静态方法的invokestatic指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的invokespecial指令
如果虚方法调用指向一个标记为final的方法,那么Java虚拟机也可以静态绑定该虚方法调用的目标方法
Java虚拟机中采取了一种用空间换取时间的策略来实现动态绑定
它为每个类生成一张方法表,用以快速定位目标方法。那么方法表具体是怎样实现的呢?
方法表(区分静态绑定和动态绑定)
在介绍那篇类加载机制的链接部分中,我曾提到类加载的准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表
这个数据结构,便是Java虚拟机实现动态绑定的关键所在
下面我将以invokevirtual所使用的虚方法表(virtual method table,vtable)为例介绍方法表的用法invokeinterface所使用的接口方法表(interface method table,itable)稍微复杂些,但是原理其实是类似的
方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法
这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法
方法表满足两个特质:
1. 子类方法表中包含父类方法表中的所有方法;
2. 子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同
我们知道,方法调用指令中的符号引用会在执行之前解析成实际引用
对于静态绑定的方法调用而言,实际引用将指向具体的目标方法
对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)
在执行过程中Java虚拟机将获取调用者的实际类型
并在该实际类型的虚方法表中根据索引值获得目标方法
这个过程便是动态绑定
在我们的例子中,Passenger类的方法表包括两个方法:
● toString
● passThroughImmigration,
它们分别对应0号和1号
之所以方法表调换了toString方法和passThroughImmigration方法的位置,是因为toString方法的索引值需要与Object类中同名方法的索引值一致
为了保持简洁,这里我就不考虑Object类中的其他方法
ForeignerPassenger的方法表同样有两行
其中,0号方法指向继承而来的Passenger类的toString方法
1号方法则指向自己重写的passThroughImmigration方法
ChinesePassenger的方法表则包括三个方法,除了继承而来的Passenger类的toString方法,自己重写的passThroughImmigration方法之外,还包括独有的visitDutyFreeShops方法
Passenger passenger = ...
passenger.passThroughImmigration();
这里,Java虚拟机的工作可以想象为导航员。每当来了一个乘客需要出境,导航员会先问是中国人还是外国人(获取动态类型),然后翻出中国人/外国人对应的小册子(获取动态类型的方法表),小册子的第1页便写着应该到哪条通道办理出境手续(用1作为索引来查找方法表所对应的目标方法)
实际上,使用了方法表的动态绑定与静态绑定相比
仅仅多出几个内存解引用操作:
1. 访问栈上的调用者
2. 读取调用者的动态类型
3. 读取该类型的方法表
4. 读取方法表中某个索引值所对应的目标方法
相对于创建并初始化Java栈帧来说,这几个内存解引用操作的开销简直可以忽略不计
那么我们是否可以认为虚方法调用对性能没有太大影响呢?
其实是不能的,上述优化的效果看上去十分美好,但实际上仅存在于解释执行中,或者即时编译代码的最坏情况中
这是因为即时编译还拥有另外两种性能更好的优化手段:
内联缓存(inlining cache)
方法内联(method inlining)
内联缓存
内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法
在之后的执行过程中:
1. 如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法
2. 如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定
在我们的例子中,这相当于导航员记住了上一个出境乘客的国籍和对应的通道,例如中国人,走了左边通道出境。那么下一个乘客想要出境的时候,导航员会先问是不是中国人,是的话就走左边通道。如果不是的话,只好拿出外国人的小册子,翻到第1页,再告知查询结果:右边
在针对多态的优化手段中,我们通常会提及以下三个术语
1. 单态(monomorphic)指的是仅有一种状态的情况
2. 多态(polymorphic)指的是有限数量种状态的情况。二态(bimorphic)是多态的其中一种
3. 超多态(megamorphic)指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态。在这个数值之下,我们称之为多态。否则,我们称之为超多态
对于内联缓存来说,我们也有对应的单态内联缓存、多态内联缓存和超多态内联缓存。单态内联缓存,顾名思义,便是只缓存了一种动态类型以及它所对应的目标方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法
多态内联缓存则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法
一般来说,我们会将更加热门的动态类型放在前面。在实践中,大部分的虚方法调用均是单态的,也就是只有一种动态类型。为了节省内存空间,Java虚拟机只采用单态内联缓存
前面提到,当内联缓存没有命中的情况下,Java虚拟机需要重新使用方法表进行动态绑定
对于内联缓存中的内容,我们有两种选择
1. 替换单态内联缓存中的纪录。这种做法就好比CPU中的数据缓存,它对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存
因此,在最坏情况下,我们用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升
2. 劣化为超多态状态。这也是Java虚拟机的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法
与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销
具体到我们的例子,如果来了一队乘客,其中外国人和中国人依次隔开,那么在重复使用的单态内联缓存中,导航员需要反复记住上个出境的乘客,而且记住的信息在处理下一乘客时又会被替换掉。因此,倒不如一直不记,以此来节省脑细胞
虽然内联缓存附带内联二字,但是它并没有内联目标方法。这里需要明确的是,任何方法调用除非被内联,否则都会有固定开销。这些开销来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧
对于极其简单的方法而言,比如说getter/setter,这部分固定开销占据的CPU时间甚至超过了方法本身。此外,在即时编译中,方法内联不仅仅能够消除方法调用的固定开销,而且还增加了进一步优化的可能性,我们会在专栏的第二部分详细介绍方法内联的内容
虚方法总结
虚方法调用包括invokevirtual指令和invokeinterface指令
如果这两种指令所声明的目标方法被标记为final,那么Java虚拟机会采用静态绑定
否则,Java虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法
Java虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致
在解析虚方法调用时,Java虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法
Java虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法
当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法
否则,Java虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定

查看18道真题和解析