运行时栈帧结构
i. 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
ii. 每一个栈帧都包括了 局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息
iii. 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
iv. 在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法
栈帧的概念结构:
(/images/jvm_stackframe_structure.jpg)
1. 局部变量表
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
- 单位:局部变量表的容量以变量槽(Slot)为最小单位
- 虚拟机定位:虚拟机通过索引定位的方式使用局部变量表,索引值从0开始到局部变量表最大的Slot数量
- 64位数据的访问:对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用热任何方式单独访问其中的某一个
- 局部变量表的空间分配:
i. 如果执行的是实例方法,在那局部变量表最后还给你第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。
ii. 其余参数按照参数表顺序排列,占用从1开始的局部变量Slot。
ii. 参数表分配完毕后,再根据方法体内部定义的变量顺序和作用于分配其余的Slot - Slot重用:如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用,但是Slot的复用会直接影响到系统的垃圾收集行为
实例代码一:1
2
3
4
5
6public static void main(String[] args)() {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
实例代码二:1
2
3
4
5
6
7public static void main(String[] args)() {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
代码一的placeholder没有被回收,而代码二的被回收了
2. 操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈写入和提取内容,也就是出栈/入栈操作
- 在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的,但大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠
(/images/jvm_stackframe_operandstack.jpg)
3. 动态链接
常量池中的符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化为静态解析;另外一部分在每一次运行期间转化为直接引用,这部分称为动态连接
4. 方法返回地址
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能保存这个计数器值;而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息
5. 附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息
方法调用
方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程
####1. 解析
调用目标在程序代码写好,编译器进行编译时就必须确定下来,这类方法的调用称为解析。
- 非虚方法和虚方法:可以在解析阶段中确定唯一调用版本的方法有4类:静态方法、私有方法、实例构造器、父类方法,他们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为 非虚方法,其他方法称为 虚方法(除了final方法)
- 解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及引用全部转变为可确定的直接引用
2. 分派
解析和分派两者之间的关系不是二选一的排他关系,他们是在不同层次上筛选、确定目标方法的过程。
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的经典应用和是方法重载。
动态分派
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
单分派与多分派
方法的接受者与方法的参数统称为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
Java的静态分派属于多分派类型,动态分派属于单分派类型,所以Java是一门静态多分派、动态单分派的语言
虚拟机动态分派的实现
最常用的“稳定优化”手段是为类在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找,虚方法中存放着各个方法的实际入口地址
!(/images/jvm_method_dispatch.jpg)
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把类的方法表也初始化完毕
基于栈的字节码解释执行引擎
解释执行
Java编译器完成了程序代码经过了词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。 这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的变异就是 半独立的实现。
基于栈的解释器执行过程
看一个例子的执行流程基本可以了解1
2
3
4
5
6public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
字节码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public int calc();
Code:
Stack=2, Locals=4, Args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
}
上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做一些优化来提高性能,实际的运作过程不一定完全符合概念模型的描述