JVM:栈帧动态连接与方法调用详解

Posted liu++

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM:栈帧动态连接与方法调用详解相关的知识,希望对你有一定的参考价值。

JVM(四):栈帧动态连接与方法调用详解

学习JVM的一些分享,希望看到的人学起JVM更快

我写的这个JVM系列算是笔记,学习的是:

视频:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)

书籍:《深入理解Java虚拟机》

说一下学习过程中遇到的问题,按照一二三章这个顺序看这本书我看不懂,第二章简单说了一下JVM运行时内存数据区,很多名词,第三章直接GC,根本不知道各个区是干嘛的怎么运行,然后就去看视频了。

视频讲解顺序是概述、类加载(对应书第七章)、详细介绍JVM运行时内存数据区各个部分,目前学到Java虚拟机栈,对应书第八章,看弹幕说的翻到了第八章,书上写的非常好。

推荐视频和书一起使用效果绝佳。

回归正题!

回顾栈帧

在这里插入图片描述

之前了解了栈帧的局部变量表,就是存局部变量的,在栈帧中另一个重要的结构是操作数栈,这个是存计算过程中的中间数据(临时结果)的,java的执行引擎在执行代码时一直操作操作数栈。

操作数栈

作用:在方法执行过程中根据字节码指令,往(操作数)栈中写入数据或提取数据,即入栈和出栈。

直接看一个例子(例子来源于宋老师视频)就理解了:15+8在java中是如何执行的。

public void testAddOperation(){
	int i=5;
	int j=8;
	int k=i+j;
}
/*对应字节码
0 iconst_5 //当int取值-1~5时,JVM采用iconst指令将常量压入栈中
1 istore_1//将栈顶元素5出栈存到局部变量表索引为1的位置 
2 bipush 8 //入栈8
4 istore_2//将栈顶元素8出栈存到局部变量表索引为1的位置
5 iload_1 //局部变量表中的slot1(5)入栈
6 iload_2//局部变量表中的slot2(8)入栈
7 iadd //栈顶两元素出栈加法操作然后结果入栈(过程用到了局部变量表)
8 istore_3//将13出栈存到局部变量表3的位置
9 return
*/

看字节码就很清楚,对与回调方法(方法里面调用其他方法),上一篇说过一个方法结束,对应栈帧出栈,它的结果会保存到栈顶栈帧中,也是通过操作数栈存储的。

如果被调用的方法带有返回值,其返回值将会被压入当前栈的操作数栈中。
在这里插入图片描述

强烈推荐大家试一试,用jclasslib看字节码很清楚。

虽然看有的帖子说可以不看字节码部分面试不问,但是也不知道怎样,而且懂了一些字节码指令之后自己看着很方便。

几个简单字节码指令

iconst_x:对于-1~5的int变量入栈用这个指令,x是具体数值。

istore_x:栈顶元素出栈存入局部变量表,x是在局部变量表中的索引。

bipush x:对于-128~127的int入栈用这个命令,x是具体值。

aload_x:读局部变量表的一个变量入栈,但是x必须是0 1 2 3 //这个不是很确定,求指导,谨慎观看。

1、aload

从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶

aload_0把this装载到了操作数栈中aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置,但取值仅可为0、1、2或者3。

2、iload_,lload_,fload_,dload_

还有一些其他相似的操作码用来装载非对象引用,包括iload_、lload_、fload_和dload_,这里的i代表int型,l代表long型,f代表float型以及d代表double型。在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload,这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置。

动态连接

作用:动态链接保存的是指向方法区常量池中该方法的引用。

方法调用

这里的方法调用指的是:确定被调用的是哪个方法!!!

动态连接和方法调用关系很大,执行.class文件遇到的都是符号引用,这样带来的好处是让Java动态扩展能力更强,不好的是让Java方法调用太复杂了。

解析——》静态连接

在类加载的连接的解析阶段.class 文件中的一部分符号引用就被转换成了直接引用放到了方法区的常量池部分!

那么哪些方法的符号引用可以在解析阶段被解析成直接引用呢?

**答:静态方法、私有方法、实力构造器、父类中的方法(super.)和final修饰的方法!它们的共同特点是无法通过继承等方式重写,也就是没办法多态!!!符合“编译期可知,运行期不可变”**这个要求。

调用静态方法的字节码指令是:invokestatic

调构造器方法、私有方法、父类中的方法的字节码指令:invokespecial

调用final方法的字节码指令是:invokevirtual,这个方法是调用虚方法的,但是final也用它调用

虚方法和非虚方法

这个概念是从c++来的,到java中可以继承重写的就是虚方法,否则就是非虚方法。

分派–》动态连接

上面说了在类加载时期,可以由符号引用解析成直接引用的方法,现在说必须在运行时才能由符号引用变成直接引用的方法。

静态分派–》重载(Overload)

这个实际上是可以解释一个问题,我们在代码中常写InterfaceService a=new InterfaceServiceImpl();,前面是父类(接口)后面是new子类(实现类),为什么可以这样写呢?前面的叫变量的“静态类型”,后面的是“实际类型”(运行时类型)!

在编译期,只知道a是aService到了执行的时候才知道a的具体类型,虽然在编译期不知道具体类型,但是可以知道可能是哪几个类型。

看个继承与重载的例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gq16tW0n-1621431813390)(C:\\Users\\Administrator\\Documents\\Tencent Files\\1766569880\\FileRecv\\MobileFile\\IMG_20210519_195036.jpg)]

虚拟机是通过传入变量的静态类型来选中使用哪个重载的方法man和woman的静态类型都是Human,所以执行哪个方法就清楚了。

重载方法还有优先级,这里就不细说了,就是根据类型分级的。

静态方法和动态方法

众所周知Java是静态方法,为什么呢?

答:java是判断变量的类型信息(静态类型),而不是变量值的类型信息(实际类型)。

还是动态语言好一点哈哈。java有反射但是只能说一般,Java8加入了lambda也是向动态语言靠拢,但是只能说一般。

动态分派–》重写(Override)

先说一下invokedynamic,java7引入的,在使用lambda表达式时使用。

调用方法的字节码指令一共五种上面说了四种了,具体罗列一下:

  • invokestatic
  • invokespecial
  • invokevirtual
  • invokeinterface
  • invokedynamic

方法重写的本质,分四步:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C

2、如果在类型C中找到与常量中描述符合简单名称都相符的方法,则进行访问权限校验,如果通过返回这个方法的直接引用,查找过程结束;不通过则返回IlleglAccessError异常。

3、否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

看第三步,会逐级查找,这是很费事的,所以jvm建立了一个虚方法表,对其记录,下次调的时候直接从表中调就行了。

虚方法表是在类加载的链接-解析阶段创建的。

参考

JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)

aload、aload_1、iload都是什么意思

视频:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)

书籍:《深入理解Java虚拟机》

以上是关于JVM:栈帧动态连接与方法调用详解的主要内容,如果未能解决你的问题,请参考以下文章

jvm内存区域之虚拟机栈

jvm内存区域之虚拟机栈

决战圣地玛丽乔亚Day38---JVM相关

JVM理论:(三/3)运行时栈帧结构基于栈的字节码解释执行过程

JVM运行时数据区与JVM堆内存模型小结

JVM 栈帧