运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的。
- 局部变量表
作用:局部变量表用于存放方法参数和方法内部定义 的局部变量。
局部变量表的容量以变量槽为最小单位,都可以使用32位或更小的物理内存来存储boolean、 byte、char、short、int、float、reference或returnAddress数据类型,其中reference表示的是一个对象实例的引用。如果是64位的数据,可以分割成2个32位读写的操作。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成 即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用。
虚拟机可以根据reference引用做
1. 从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引
2. 根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
虚拟机怎么是使用局部变量表?
通过索引定位
的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位 数据类型的变量,则说明会同时使用第N和N+1两个变量槽。
变量槽可以重用?怎么重用?
变量槽可以重用,主要用来节省栈帧消耗的内存空间。当变量不再被访问时,槽的内容就能被复用,如下的代码:
此时虽然placeholder的作用域被限制在花括号以内,但并没有被回收掉。而下面这段代码placeholder被回收:
placeholder能否被回收的根本原因:
局部变量表中的变量槽是否还存有 关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,
但在此之后,再没有发生过任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量 所复用,所以作为GC Roots一部分的
局部变量表仍然保持着对它的关联。
如果一个方法的代码 有一些耗时很长的操作,实际上已经不会再使用的变量,手动将其设置为null值
-
操作数栈
操作数栈是一个后入先出栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种 字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。
两个栈帧出现重叠的目的:1.节约一部分空间 2.方法调用时可以直接共用部分数据。
3.动态连接、静态解析:
1. 静态解析:类加载阶段或第一次使用时,符号引用转化为直接引用。
2. 动态连接:一部分符号引用在每一次运行期间都转化为直接引用。
方法调用
任务:确定被调用方法的版本 (即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。
1. 解析:将其中的一部分符号引用转化为直接引用,但这种解析成立的前提:调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本。即Java中静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引 用解析为该方法的直接引用。
2.分派
1.静态分派:依赖静态类型来执行方法版本的分派动作
Human man =new Man();
Human就是这个对象的静态类型,Man是动态类型。
典型表现:方法重载,很多情况下这个重载版本并不是“唯 一”的,往往只能确定一个“相对更合适的”版本。静态方法会在编译期确 定、在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通 过静态分派完成的。
2.动态分派:动态分派与实际类型相关。
典型表现:重写,吧常量池中符号引用解析成直接引用,根据方法接受者的实际类型来选择调用的方法版本。
怎么实现动态分派?
动态分派执行动作很繁琐,我们可以是为类型在方法 区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以 提高性能。具体如下:
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方 法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了 这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。
如果方法具有相同签名,则父类、子类的虚方法表中都应当具有一样的索引序 号。
虚方法表一般在类加载的连接阶段进行初始化。
3.Java虚拟机是如何根据实际类型来分派方法执行版本的呢?
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质
4. 单分派和多分派
方法的接收者与方法的参数统称为方法的宗量,单分派是根据一个宗量对 目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
在Java中,静态分派属于多分派,它需要根据 静态类型、方法参数来进行选择。动态分派是单分派,只需要根据方法接受者的实际类型进行选择就可以。
Java语言是一门静态多分派、动态单分派的语言