文章目录
一、虚拟机栈描述栈的存储单位栈帧栈帧内容局部变量表slot操作数栈(Operand Stack)字节码demo栈顶缓存技术动态链接(指向运行时常量池的方法引用)静态链接动态链接虚方法表方法返回地址
一、虚拟机栈
包含java方法(局部变量表,操作数栈,动态链接,方法出口)
描述
Java的指令都是根据栈来设计的,优点是跨平台,指令集小,编译容易实现,缺点是
性能下降,实现相同的功能需要更多的指令
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,是线程私有的
一个栈帧就对应的一个方法
生命周期和线程一样
作用:
主要管java程序的运行,它保存方法的局部变量(引用类型的话,则是引用的地址),部分结果,
并参与方法的调用和返回
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。不存在垃圾回收。
jvm直接对java栈的操作,每个方法执行,伴随着进栈,结束后出栈。
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
jvm直接对java栈的操作只有两个:
每个方法执行,伴随入栈和出栈
执行结束后的出栈
虚拟机栈是存在oom的,但是不存在gc
栈的存储单位
栈中存的是什么?
每个线程都有自己的栈,栈中的数据·都是以栈帧的格式存在的
在这个线程上正在执行的·每个方法都各自对应一个栈帧,
栈帧是一块内存区域是一个数据集,维系着方法执行过程中的各自数据信息
栈帧
由于是栈,所以是先进后出,后进先出
在一条线程中,一个时间点,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧,
栈帧对应的是方法 ,当前栈帧对应的方法就是当前方法
执行引擎运行的所有字节码指令只针对当前栈帧进行操作,如果该方法执行中调用了其他的方法,
那么对应的新的栈帧就会被创建出来放在栈的顶端,成为当前栈帧,
也就是调用了方法那么就会创建新的栈帧,并且成为当前栈帧,方法执行完后返回,
如果有值则也会携带回来给前一个栈帧,然后会抛弃当前栈帧,重新回到调用方法的对应栈帧
方法有两种返回函数的方式:
一种是正常的函数返回,使用
return指令;
另一种抛出异常,但都会导致栈帧被弹出。
使用了异常
try catch的也算是正常返回,没做异常处理的才是另一种返回方式,
抛出了异常,出现了异常不一定会导致线程结束,要看当前栈帧是否为最后一个栈帧,也就是main方法对应的,如果不是则会向上抛出(也就是执行上一个栈帧),并且弹出当前栈帧
这里的上一个栈帧指的是调用当前栈帧的栈帧,按照栈的结构来说,就是栈顶下面的栈帧
栈帧内容
局部变量表
(Local Variables
)
操作数栈
(Operand Stack
) 或表达式栈
动态链接
(Dynamic linking
) 或指向运行时常量池的方法引用
方法返回地址
(Return Address
) 或方法正常退出或者异常退出的定义
一些附加信息
五个部分注组成
局部变量表
局部变量表也被称为局部变量数组或本地变量表
。定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,
这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型
由于局部变量表是建立在栈上的,是线程私有数据,因此不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的
Maximum local variables数据项中,在方法运行期间是不会该变局部变量表的大小的
可以使用反编译查看 javac
-v 字节码文件
栈越大,方法嵌套调用的次数就越多,对于一个函数来说,
它的参数和局部变量越多,局部变量表就越大,它的栈帧就越大
方法执行时,虚拟机通过局部变量表完成参数值到参数变量表的传递过程,方法介素,跟随栈一起销毁
slot
注意,查看字节码指令时,字节码指令所标识的位置是代码的行数,第n行
。参数值的存放总是在局部变量数组的index0开始的,到数组长度
-1的索引结束
。局部变量表,最基本的存储单元是slot(变量槽)
。局部变量表中存放编译期可知的各种基本数据类型,引用类型,returnAddress类型
。在局部变量表里,
32位以内的类型只占用一个slot(包括returnAddress类型),
64位的类型
long,double占用两个slot
Byte
,short,char,在存储前被装换位
int,
boolean转换为
int,
0表示
false,非
0表示
true
Long
,double占据两个slot、
引用类型也占
32位
。Jvm会为局部变量表中的每一个slot都分配一个访问索引,
通过这个索引即可成功访问到局部变量表中指定的局部变量值
。当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被
复制到局部变量中的每一个slot上
。如果需要访问局部变量表中一个
64bit的局部变量值时,只需要使用前一个索引即可,
比如
long或
double 占用了,
3,
4,索引,那么需要通过起始索引
3就可以访问了
因为一个槽的大小是
4个字节,
。刚当前栈是由构造方法或者实例方法创建的,那么该对象引用
this将会放在index为
0的slot处,其余的参数按照参数顺序继续排列
每一个方法都对应着一个栈帧,而每一个栈帧里面有一个局部变量表
局部变量是包含,方法参数。方法内定义的变量
(包括引用和非引用类型
)
注意,非静态方法里面起始还有一个
this的变量,放在槽的索引为
0的位置上,
但是静态方法对于的栈帧里的局部变量表里没有该参数,
this是属于对象的,
所以也就解释了为什么在
static方法里是无法使用
this关键字的
还有就是关于局部变量表中的槽位是可以重复使用的,如果一个局部变量过了它的作用域,
那么在其作用域之后声明的局部变量很有可能会复用过期的局部变量的槽位,达到节省资源的目的
如,定义一个代码块
{
Int i
=20;
i
++;
}
括号就是它的作用域范围
在准备阶段为类静态变量设置默认初始值,类初始化为类的静态变量赋值和执行静态代码快,
静态变量的加载是按顺序的,因为初始化是会执行静态变量和构造方法的,
因此静态变量和构造方法的先后需要注意
注意,类的初始化有着所有类变量的赋值动作,准备阶段是静态变量,初始化就包含非静态变量了
实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量,使用前必须要显式的赋值,否则编译不通过,因为不会为局部变量赋默认值
补充:
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表,在方法执行时,
虚拟机使用局部表变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈(Operand Stack)
每一个栈帧包含一个操作数栈,也叫表达式栈
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈
/出栈
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用后在将结果压入栈,比如,交换,求和等
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,每一个新的栈帧
也会被随之创建出来,这个方法的操作数栈是空的,但是已经创建了。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,所需的最大深度在编译器就定义好了,
保存在方法的code属性中,为max_stack
/的值
栈中的任何一个元素都是可以任意的java数据类型
32位的占一个栈深度
64位的占两个栈深度
操作数栈的结构是数组,但是,并非使用索引的方式来操作的,而是通过标准的入栈和出栈来操作的
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈的操作数栈中,并更新pc寄存器中下一条需要执行的指令
操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
另外虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
字节码demo
分析
看下面一段代码
public class ddddd {
public void ss(){
int i
=9;
int j
=8;
int k
=i
+j
;
}
}
我们查看它的字节码信息
,查看字节码工具我使用的是idea的插件jclasslib
先看字节码指令
0 bipush
9
2 istore_1
3 bipush
8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
代表的意思:
注意,刚开始时,操作数栈和局部变量表都是为空的
0:将i变量push进入了操作数栈
2:istore_1
,将操作数栈的值弹出放到局部表变量表中,
1表示索引为
1位置,
由于该方法是非静态的,所有为
0的位置被
this占用了,此时操作属栈空了,
3:将变量j压入栈中,
5:将j弹出放入到局部变量表中
6:将局部变量表的索引为
1的值取出,放入栈中
7:将局部变量表的索引为
2的值取出,放入栈中
8:iadd
,会将这两个值弹出,进行运算,会使用到执行引擎来计算,然后将计算的值压入栈
9:将值弹出放到局部变量表索引为
3的位置上,
10:没有返回值,最后就
return结束
栈顶缓存技术
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,完成一项操作需要使用更多的入栈和出栈,
增加了内存的读写次数,操作数存在内存中,因此频繁的操作读写会影响执行的速度,
所以就有了栈顶缓存的技术,将栈顶元素全部缓存在物理cpu的寄存器上,
以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接(指向运行时常量池的方法引用)
每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如:invokedynamic指令
在java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在calss文件的常量池里,
比如,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,
那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
也就是说当程序运行时,就会将
class文件的常量池放到方法区里,由于是运行是加入到方法区的,
所以叫运行时常量池,而动态链接存放的则是指该运行时常量池的方法引用
为什么需要常量池? 就是为了提供一些符号和常量,便于指令的识别
方法的调用 在jvm中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
静态链接
当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译器可知,且运行期保持不变时,
这种情况下将调用方法的符号引用转换为直接引用锁的过程称之为静态链接,
在加载阶段就可以将符号引用转为直接引用
动态链接
如果被调用的方法在编译器无法被确定下来,也就是说,只能在运行期将调用的符号引用转换为直接引用,
由于这种引用转换过程具备动态性,所以叫动态链接
他们对应着的是早期绑定和晚期绑定
绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,
这仅仅发生一次早期对应着静态链接,晚期对应着动态链接
举例,动态链接:如接口的实现子类覆写的抽象方法等,或者类的子类覆写的方法等
静态链接:如构造器,父类构造器,都是可以在编译器就可以确定的
如果不想拥有晚期绑定的这种特征,可以使用
final来修饰方法,使得无法覆盖,编译器就可以确定
非虚方法和虚方法,对应的也是前期和晚期绑定
静态方法,私有方法,
final方法,实例构造器,父类方法都是非虚方法,其他都是虚方法
虚拟机提供的方法调用指令
invokestatic
:调用静态方法,解析阶段确定下来的
Invokespecial
:调用
<init>方法。私有及父类方法,解析阶段确定唯一方法版本
Invokevirtual
:调用所有虚方法
Invokeinterfack
:调用接口方法
前两个是非虚方法的,后两个是虚方法的
注意,在调用
final方法时,字节码指令显示的是Invokevirtual,但实际上不是,它是
final修饰,
是无法覆写的,编译器就可确定的
Jdk1
.7增加了一个invokedynamic
:指令
动态解析出需要调用的方法,然后执行前面四条指令固化在虚拟机内部,
方法的调用执行不可认为干预,为这个指令则支持用户确定方法版本,
这是java为了实现【动态类型语言】支持而做的一种改进,然而
1.7并没有提供直接生成该指令的方法,
而是到了
1.8的lambda表达式的出现,该指令才有了直接生成的方式
动态类型语言和静态类型语言:
最大的区别在于对类型的检查是在编译器还是在运行期,编译器检查则是静态语言,、
反之动态语言
静态类型语言是判断变量的类型信息,动态类型语言是判断变量值的类型信息,
变量没有类型信息,变量值才有类型信息
虚方法表
在面向对象编程种,会频繁的使用到动态分配,如果在每次动态分派的过程种都要重新在类的方法元数据中搜索合适的目
标的话就可能影响到执行效率,因此,为了提高性能,jvm采用在类的方法区建立一个虚方法表,
非虚方法不会出现在表中,使用索引表来代替查找
每一个类都有虚方法表,表中存放着各个方法的实际入口
那么虚方法表什么时候创建
会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成后,jvm会把该类的虚方法表也会初始化完毕
方法的重载是静态分派,方法的覆写是动态分派,
如果一个类继承了一个类,覆写了父类的一些方法,我们知道虚方法表存放的是非虚方法,
而且是特定方法实际真正的入口地址,如果没有覆写父类方法,那么虚方法表就直接指向了没有重写的父类方法,
而并非非将父类方法拷贝一份到方法表中,这样做节省了空间,如果覆写了,方法就会存在自己类的虚方法表中
对于方法重载,其实它是一种静态的行为,那么调用方法传的参数则只会认变量的静态类型,
而并不是变量的实际类型,字节码指令是invokevirtual
方法返回地址
存放调用该方法的pc寄存器的值
方法的结束有两种,前面已经说过了
无论哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,
调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条的地址,
而通过异常退出的,返回地址是要通过异常表来确定的,栈帧一般不会保存这部分的信息
方法开始执行后,只有两种方式可以退出方法
1.执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,正常完成出口
返回的指令是更据方法的返回值的实际数据类型确定的
有:ireturn
(当返回值是
boolean,byte,char,short,和
int类型
),lreturn
,freturn
,dreturn以及areturn,
还有一个
return,是给方法的返回值为
void的,以及实例初始化方法,类和接口的初始化方法使用的
2.在方法执行的过程中遇到了异常,并且这个异常没有在方法内部进行处理,
也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,
是异常完成出口方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,
方便再发生异常的时候找到处理异常的代码
注意,静态代码块的返回指令是
return ,是由cinit构造完成的,构造块是在构造器init里面处理的