1.虚拟机栈
1.1概述
作用:主管java程序的执行,存储的是栈帧(稍后介绍),栈帧主要存储局部变量,部分结果,以及方法的调用和返回
特点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- jvm对虚拟机栈的操作只有两个,伴随着方法的执行入栈、结束出栈
- 不存在垃圾回收问题
栈中可能会出现的问题
根据java虚拟机规范,虚拟机栈的大小是动态或者固定不变的
- 如果采用固定的栈,每个线程的虚拟机栈可以在线程创建的时候独立完成,如果线程请求分配的栈容量超过了虚拟机栈允许的最大容量,会抛出StackOverflowError(堆栈溢出)
- 如果采用动态的栈,在尝试扩展的时候无法申请足够的内存,或者在创建新线程时没有足够的内存创建对应的虚拟机栈,就会抛出OutOfMemoryError(内存溢出)
1.2栈帧的存储单位
- 每个线程都有自己的栈,栈中的数据都以**栈帧(Stack Frame)**的形式存在
- 在当前线程上正在执行的方法都有各自对应的栈帧
- 栈帧是一块内存区域、数据集,维护着方法执行过程中的各种数据信息
1.3栈运行原理
- jvm对栈的操作只有两个,压栈\出栈,遵循”FILO“原则
- 一个正在运行的线程,只会有一个运行的栈帧,就是当前方法执行的当前栈帧,也叫栈顶栈帧,与当前栈帧对应的就是当前方法,与当前方法对应的就是当前类
- 执行引擎运行的所以字节码文件只对当前栈帧进行操作
- 不同线程的栈帧是不允许相互引用的,所以不可能在一个栈帧中引用另一个线程的栈帧
- 如果方法中调用了其他方法,对应的栈帧也会创建出来,成为新的当前栈帧,放在栈顶
- 如果方法中调用了其他方法,方法返回的时候,当前栈帧会返回一个执行结果给前一个栈帧,随后虚拟机会丢弃当前栈帧,让前一个栈帧成为当前栈帧
- java中有两种方法返回方式:1,return。2,抛异常。两种方式都会导致栈帧被弹出
- idea在执行debug的时候,可以在Frames窗口各个栈帧的压栈、出栈
1.4栈帧的内部结构
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 其他附加信息
1.4.1局部变量表
主要用于存储方法参数和方法体中的局部变量
可以存储的类型有:
- java基本数据类型
- 对象引用(可能是指向对象地址的引用指针,或是指向代表对象的句柄或相关地址)
- returnAddress类型(指向方法返回的字节码指令,已被异常表取代)
由于局部变量表是存在线程的栈上的,是线程私有的数据,所以不存在数据安全问题
局部变量表的容量大小在编译期就确定下来的,并保存在方法的Code属性的maxmum local variables数据项中,运行期是不会改变表的大小的
方法嵌套调用的次数由栈的大小决定,一般情况下,栈越大,方法嵌套调用越多。
1.4.1.1槽 slot
- 局部变量表的基本单位是slot(变量槽)
- 在局部变量表中,32位的数据类型占用一个slot,64位则占用两个slot
注:
byte,short,char,boolean(0表示false,1表示true)在存储之前会转为int
long和double占两个slot
- jvm会为局部变量表中的每个slot分配一个索引,通过索引访问对应的slot
- 如果需要访问一个64位的局部变量时,只需要前一个索引即可。如(long类型的变量占用了a1,a2两个slot,此时需要访问a1即可访问到该变量)
- 如果当前栈帧是由构造方法或实例方法创建的,那么this将会存放在index为0的slot中(此处引出一个问题:静态方法为什么不能引用this?就是因为this变量不存在当前方法的局部变量表中)
- 在栈帧中,与性能调优关系最为密切的就是局部变量表。在方法执行时,虚拟机会使用局部变量表来完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
- slot是可以重用的
上图:局部变更表中有四个变量
注:至于这里为什么this没有在第一个位置,是因为在执行构造方法之前会先执行非静态代码块
上图:但是局部变量最大槽数只有3,因为代码块中变量b使用完就销毁了,但是b的slot还存在,所以就被变量c重用了
拓展:图片中的方法为构造方法,在字节码文件中构造方法叫init,静态代码块叫clinit,而非静态代码块就没别的名字,因为它里面的局部变量会被存在init方法对于栈帧中
1.4.2操作数栈(LIFO)
- 在方法执行过程中,根据字节码指令,对操作数栈写入(入栈push)和提取(出栈pop)数据。
- 某些字节码指令将值写入操作数栈,某些指令将值取出栈,再进行相关操作,比如加减乘除。结果值也会放入操作数栈。
概述:
- 操作数栈,主要用于保存计算过程的中间结果,同时作为变量计算后的临时的存储空间
- 操作数栈是jvm执行引擎的一个工作区,当一个方法执行时,新的栈帧出现,此时这个操作数栈是空的
- 操作数栈都会有一个明确的大小来用于存储数值,所需要的最大容量再编译器就定义好了,在方法的Code属性的max_stack中
- 栈中的元素可以是java中的任意数据类型,与slot一样(32位占一个单位,64占两个单位)
- 如果被调用的方法有返回值,那返回值会被压到当前栈帧的操作数栈中,并通知程序计数器下一条需要执行的字节码指令
- jvm的解释引擎是基于栈的执行引擎,栈指的是操作数栈
1.4.3动态链接(指向常量池的方法引用)
静态绑定:当一个字节码文件被加载到虚拟机时,如果其调用的方法在编译器可知,并且在运行期保持不变
动态绑定:相反,如果在编译器不能确定下来,只能在运行期将符号引用转为直接引用
- 每个栈帧中都包含一个指向常量池中该栈帧所属方法的引用,目的就是为了实现动态链接
- 在java文件被编译成字节码文件时,所以变量和方法引用都会作为符号引用保存在Class文件的常量池中
- 比如:描述一个方法调用其他方法时,就是通过常量池中指向方法的符号引用来表示,动态链接的作用就是将这些符号引用转换为调用该方法的直接引用
1.4.3.1jvm时如何调用方法的
- 方法调用阶段的唯一任务就是找到需要调用发方法,暂时还不涉及内部运行过程
- 方法在Class文件中存储的是符号引用,而不是在运行时的直接引用,也就是需要在类加载阶段,甚至是运行期才能确定方法的直接引用
1.4.3.1.1在jvm中方法的符号引用转化为直接引用跟绑定机制有关
绑定机制有早期绑定(静态绑定)、晚期绑定(动态绑定),绑定是一个字,方法或类的符号引用转为直接引用的过程,只会发生一次
1.4.3.1.2虚方法和非虚方法
- 在编译期可以确定下来,在运行期不变,称为非虚方法。比如,静态方法、final方法、私有方法、父类方法,构造方法都是非虚方法
- 其他方法叫虚方法
1.4.3.1.3虚方法表
- 为了提高性能,jvm在类的方法区建立了一个虚方法表,用于存放虚方法。因为在面向对象编程中,会频繁设计动态分配,每次动态分配都在类的方法元数据中查找合适的目标。
- 每个类都会有一个虚方法表,存放着各个方法的实际入口
- 虚方法表会在类加载的连接(验证、准备、解析)阶段开始初始化,类的变量初始值准备完成之后,虚方法表也初始化完毕
1.4.4f方法返回地址
-
用来存放调用该方法的程序计数器值
-
方法的两种返回方式,不管是那种,在方法推出后都会返回到调用该方法的位置
1.正常返回时:调用者的程序计数器的值作为返回地址,就是调用该方法的指令的下一条指令地址 2.异常返回时:通过异常表来确定的,栈帧中一般不会保存这部分信息
-
实际上,方法的退出就是当前栈帧出战的过程,此时需要回到上一个方法,让方法执行下去
-
正常返回与异常返回的区别在于:异常返回不会给上层调用者任何的返回信息
1.4.5附带信息
栈帧中还可以携带一些jvm相关信息。比如,对程序调试支持的信息,但是这些信息取决于虚拟机的实现