深入理解jvm-栈帧&方法调用

本文为读书笔记

1. 基本概念

在这里插入图片描述
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(VirtualMachine Stack)的栈元素。

**基本组成:**局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中

以Java程序的角度来看,同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为**“当前栈帧”**(CurrentStack Frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。

springboot源码里有一个部分:

这一部分是SpringApplication.class 的构造方法里,得到main方法入口的类,其操作就是获取到当前栈帧;debug可以明显看到当前栈帧
在这里插入图片描述
基本流程就是创建一个运行时异常,然后获得堆栈数组,遍历StackTraceElement数组,判断方法名称是否为“mian”,如果过是则通过Class.forName()方法创建Class对象。

代码很简单,但是SpringBoot的使用方法是否让我们觉得很有启发性呢。下面对照一下Java的异常处理,具体了解一下StackTrace的使用。

Stacktrace(堆栈跟踪)是一个非常有用的调试工具。在程序出现异常或手动抛出异常时,可以显示出出错的地方,引起错误的层级关系。

2. 局部变量表

用于存放方法参数和方法内部定义的局部变量的存储空间。
在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

对于32位的boolean、byte、char、short、int、float、reference[插图]和returnAddress 一个槽就够了。
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。

Java虚拟机通过索引定位的方式使用局部变量表,**索引值的范围是从0开始至局部变量表最大的变量槽数量。**如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。

局部变量表中的变量槽是可以重用的
重用举例:
在这里插入图片描述
如果没有 int a= 0;是不会触发gc的,有了之后才会触发gc;
原因:
局部变量表中的变量槽是否还存有关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,再没有发生过任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。

3. 操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。

操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。

4. 动态连接

每个栈帧都包含一个指向运行时常量池[插图]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

5. 方法返回地址

只有两种方式退出方法:1.正常返回 并向上层调用者返回值 2.抛出异常并且异常没有得到妥善处理
一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

6. 附加信息

调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现
一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息

举例:i++, ++i

也叫做基于栈的解释执行

int a = 10;
 int b = a++ + ++a + a--; 
 System.out.println(a); 
 System.out.println(b);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图片来自b站黑马解密jvm

7.方法调用

Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。

在Java虚拟机支持以下5条方法调用字节码指令,分别是:·invokestatic。用于调用静态方法。

·invokespecial。用于调用实例构造器<init>()方法、私有方法和父类中的方法。

·invokevirtual。用于调用所有的虚方法。

·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。

·invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-VirtualMethod),与之相反,其他方法就被称为“虚方法”(Virtual Method)。

分派

Man man =new Women();
Man 为静态类型,Women为动态类型;

所以一个方法的执行是按照进左边的静态类型,还是右边的动态类型,分为静态分派和动态分派;
**静态分派的最典型应用表现就是方法重载。**静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。

动态分派的实现过程,它与Java语言多态性的另外一个重要体现——重写(Override)有着很密切的关联。

动态类型语言

动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的
“变量无类型而变量值才有类型”

下面是java内部的实现动态的方法

java.lang.invoke包 执行A内的println()

在这里插入图片描述

看起来像反射,但
MethodHandle在使用方法和效果上与Reflection有众多相似之处:

·Reflection和MethodHandle机制本质上都是在模拟方法调用,但是**Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。**在MethodHandles.Lookup上的3个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual(以及invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。

·**Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。**前者是方法在Java端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。而后者仅包含执行该方法的相关信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。

·由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还在继续完善中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。

调用祖类的方法:
MethodHandles.Lookup类 类似于Unsafe类的获取,这个类是new不出来的
在这里插入图片描述

发布了37 篇原创文章 · 获赞 6 · 访问量 4640

猜你喜欢

转载自blog.csdn.net/littlewhitevg/article/details/105539789
今日推荐