JVM运行时数据区篇(虚拟机栈帧结构)
Posted ProChick
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM运行时数据区篇(虚拟机栈帧结构)相关的知识,希望对你有一定的参考价值。
JVM运行时数据区篇
一、局部变量表
1.什么是局部变量表?
- 局部变量表也被称之为局部变量数组或本地变量表
- 局部变量表被定义为一个数字数组(
为什么叫做数字数组呢?因为任何基本数据类型都可以用数值表示,而引用类型存储的主要是地址
),主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用(reference)、返回值类型 - 局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
- 局部变量表所需的容量大小(
变量的个数
)是在编译时就被确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
2.注意细节
- 局部变量表中的变量只在当前方法区域内有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,局部变量表就越大,也就是说它的栈帧就越大。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
- 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
3.程序演示
利用javap -v命令对字节码文件进行解析,查看局部变量表信息
利用 jclasslib byte viewcoder 插件查看方法内部结构信息
4.什么是变量槽?
-
变量槽( slot )是局部变量表最基本的存储单位
-
在局部变量表里,32位以内的数据类型只占用一个slot(
byte、short、char、int、float、boollean、引用类型
), 而64位的数据类型占用两个slot(long、double
) -
JVM会为局部变量表中的每一个
slot
都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定位置的局部变量值 -
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照定义顺序被复制到局部变量表中的每一个
slot
上,值的存放总是在局部变量数组的索引0
位置开始,到数组长度-1
的位置索引结束 -
如果需要访问局部变量表中一个64位的局部变量值时,也只需要使用它的第一个索引访问即可
-
如果当前栈帧是由构造方法或者实例方法创建的( 也就是非静态方法 ),那么该对象自身的引用变量
this
将会存放在局部变量表索引为0的slot
处,其余的参数仍然按照定义顺序排列。这也说明了为什么静态方法中不能使用this
变量,因为在静态方法中,局部变量表没有存储该数据 -
变量插槽
slot
是可以重复利用的,如果一个局部变量离开了它的作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的public void test() { int a = 0; { int b = 0; b = a + 1; } // 此时变量c就会存储在已经被销毁的变量b所占据的slot位置 int c = a+1; }
5.变量有哪些分类?
按照数据类型分
1. 基本数据类型
2. 引用数据类型按照在类中声明的位置分
- 成员变量:在使用前,都经历过默认初始化赋值
- 被
static
修饰叫做类变量:在类加载器中链接的准备阶段给类变量默认赋值,然后初始化阶段给类变量显式赋值或静态代码块赋值 - 不被
static
修饰叫做实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
- 被
- 局部变量:在使用前,必须要进行显式赋值
- 成员变量:在使用前,都经历过默认初始化赋值
二、操作数栈
1.什么是操作数栈?
-
每一个独立的栈帧中除了包含局部变量表以外,还包含一个先进后出的操作数栈,也可以称为表达式栈,它是利用数组结构实现的
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,其中的操作数栈默认是空的
-
操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译的时候就明确了,保存在方法的code属性中为max_stack的值
-
操作数栈和局部变量表插槽类似,32位的类型占用一个栈单位深度,64bit的类型占用两个栈深度单位
-
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证
-
在方法执行的过程中,根据字节码指令,往栈中写入数据或取出数据,即入栈或出栈
比如下方的push、load操作就是入栈,store操作就是出栈
2.有什么作用?
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
3.注意细节
- 操作数栈虽然采用数组结构实现,但并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
4.过程举例
5.涉及的问题
基于操作数栈解释
i++
和++i
的区别
6.涉及的技术
- 我们知道基于栈式架构的虚拟机所使用的零地址指令更加紧凑,也就是指令集小,但完成一项操作的时候必然需要使用更多的入栈和出栈指令。这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数,也就是说执行引擎调用操作数栈的次数更加频繁。由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度
- 为了解决这个问题,HotSpot JVM的设计者们提出了
栈顶缓存技术ToS(Top-of-Stack Cashing)
,即将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
三、动态链接
1.什么是动态链接?
-
每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用, 那么包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic、invokevirtual、invokespecial等字节码指令
-
在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里, 那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
-
而常量池的作用,就是为了提供一些符号和常量,便于指令的识别
2.方法调用机制
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次
-
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译的时候就能够确定,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
-
动态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译的时候不能够被确定, 也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用。 由于这种引用转换过程具备动态性,因此也就被称之为动态链接
-
早期绑定
当被调用的目标方法如果在编译的时候就能够确定,且运行期保持不变时,即可将这个方法与所属的类型进行绑定。这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用,那么这个过程就叫做早期绑定
-
晚期绑定
当被调用的目标方法如果在编译的时候不能够被确定, 只能够在程序运行期间根据实际的类型动态的绑定相关的方法 。因此也就可以使用动态链接的方式将符号引用转换为直接引用,那么这个过程就叫做晚期绑定
3.方法调用指令
- invokestatic:调用静态方法
- invokespecial:调用构造器、私有方法、父类方法
- invokevirtual:调用虚方法
- invokeinterface:调用接口方法
- invokedynamic:动态解析出需要调用的方法,然后执行
上面前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而
invokedynamic
指令则支持由用户动态确定方法版本。
invokestatic
指令和invokespecial
指令调用的方法称为非虚方法,其余的指令(final
修饰的除外)称为虚方法。
4.invokedynamic指令详解
- JVM字节码指令集一直比较稳定,一直到JDK7才增加了一个叫做
invokedynamic
的指令,这是Java为了实现动态类型语言
支持而做的一种改进 - 但是JDK7中并没有提供直接生成
invokedynamic
指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic
指令。直到JDK8的Lambda
表达式的出现,invokedynamic
指令的才有了直接生成的方式 - JDK7中增加的动态语言类型支持的本质是对JVM规范的修改,而不是对Java语言规则的修改,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器
5.什么是虚方法和非虚方法?
- 如果方法在编译的时候就确定了具体的调用版本,且这个版本在运行时是不可变的,这样的方法称为非虚方法。常见的:静态方法、私有方法、final修饰的方法、实例构造器、父类方法都是非虚方法
- 非虚方法之外的其他方法称为虚方法,常见的:多态方法( 重写的方法 )
6.什么是动态类型语言和静态类型语言?
- 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期间还是在运行期间,编译期间就是静态类型语言,运行期间就是动态类型语言
- 静态类型语言是判断变量本身的类型信息,而动态类型语言是判断变量值的类型信息
- Java语言是静态类型语言(
变量的定义必须指明类型
),而javascript、Python语言是动态类型语言(变量的定义不需指明类型,比如:var关键字
)
7.什么是虚方法表?
- 在面向对象编程中,会很频繁期使用到动态分派,如果每次动态分派的过程都要重新在类的方法元数据中搜索合适的调用目标,那么就可能影响到执行效率。因此,为了提高执行性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)的办法来实现,进而使用索引表来代替查找。
- 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
- 虚方法表会在类加载的链接阶段被创建并开始初始化, 类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕并保存在方法区中
8.了解方法重写的本质
-
首先找到操作数栈的第一个元素所执行的对象的实际类型,记作C
-
如果在常量池中找到与类型C的描述、名称都相符的方法,则进行访问权限校验。如果通过则返回这个方法的直接引用,反之返回 java.lang.IllegalAccessError 异常
IllegalAccessError:程序视图访问或修改一个属性或调用一个方法,但是这个属性或方法你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变
-
如果在常量池中没有找到与类型C的描述、名称都相符的方法,按照继承关系从下往上依次对类型C的各个父类进行进一步的搜索和验证
-
如果始终没有找到对应的方法,则抛出 java.lang.AbstractMethodError 异常
四、本地返回地址
1.有什么作用?
用于存放调用该方法的PC寄存器的值
2.方法退出的方式
- 一个方法的结束退出有两种方式:正常执行退出、异常执行退出
- 无论通过哪种方式,在方法结束后都返回到该方法被调用的位置。方法正常退出时,返回地址为调用者PC计数器的值( 即调用该方法的指令的下一条指令地址 )。方法异常退出时,返回地址则是通过异常表来确定的,栈帧中一般不会保存这部分信息
- 正常执行退出和异常执行退出的区别在于:通过异常执行退出的不会给它的上层调用者产生任何的返回值
- 事实上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,目的是使调用者方法继续执行下去
3.方法正常退出
- 执行引擎遇到任意一个方法返回的字节码指令( return指令 )时,就会有返回值传递给上层的方法调用者,简称正常完成退出
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定
- 当返回值是boolean、byte、char、short、int类型时,返回指令为
ireturn
- 当返回值是float类型时,返回指令为
freturn
- 当返回值是double类型时,返回指令为
dreturn
- 当返回值是long类型时,返回指令为
lreturn
- 当供声明为void的方法、实例初始化方法、类和接口的初始化方法使用时,返回指令为
return
程序举例
4.方法异常退出
-
在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜素到匹配的异常处理器,就会导致方法退出,简称异常完成退出
-
如果方法执行过程中对抛出的异常进行了异常处理(
有try catch语句
),那么在当前方法中会存储一个异常处理表,方便在发生异常的时候找到处理异常的代码
程序举例
五、附加信息
其实栈帧中还允许携带与JVM实现相关的一些附加信息,当然这个并不一定存在。例如,对程序调试提供支持的信息
以上是关于JVM运行时数据区篇(虚拟机栈帧结构)的主要内容,如果未能解决你的问题,请参考以下文章