第五章:虚拟机栈
虚拟机栈出现的背景:
由于跨平台的特性,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器实现容易,缺点是性能下降,实现同样的功能需要更多的指令。
内存中的栈与堆:
栈是运行时的单位,堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
堆解决的是数据存储的问题,即数据怎么存放,存放在哪儿。
虚拟机栈基本内容:
①Java虚拟机栈是什么?
Java虚拟机栈,早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的java方法调用。
并且是线程私有的。
②生命周期:
生命周期和线程一致。
③ 作用:
主管java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
局部变量 vs 成员变量(属性)
基本数据变量 vs 引用类型变量(类、数组、接口)
④栈的特点:

但是栈存在OOM问题。
虚拟机栈的常见异常以及如何设置栈的大小:

如何设置栈的大小:
使用参数-Xss选项来设置线程的最大栈空间。
栈的存储结构和运行原理
栈中存储什么:
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
在该线程上正在执行的每个方法都有各自对应的一个栈帧。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理:

不同线程的栈帧是不能相互引用的,即不可能在一个线程的栈帧中引用另一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常,不管使用哪种方式,都是导致栈帧被弹出。
栈帧的内部结构
每个栈帧中存储着:
① 局部变量表(Local Variables)
② 操作数栈(Operand Stack)(或表达式栈)
③ 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
④ 方法返回地址(Return Address)(或方法正确退出或者异常退出的定义)
⑤ 一些附加信息

局部变量表结构的认识
局部变量表也称为局部变量数组或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。这些数据类型包括8中基本数据类型、对象引用(reference),以及ReturnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在线程安全性问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。



字节码中方法内部结构的剖析
LineNumberTable:
字节码指令的行号与代码行号的对应关系
LocalVariablesTable:(局部变量表)
startPC还是字节码指定的行号,length指的是该变量的作用范围,index指的是该变量在表中的索引。
后面还有一个描述该变量属于哪种类型的列。
变量槽slot的理解
局部变量表中,最基本的存储单元是slot。(变量槽)
参数值的存放总是在局部变量表数组的index 0处开始,到数组长度-1的索引结束。
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
(byte、short、char在存储之前被转换成int,boolean也被转换为int,0代表false,非
0表示true)
关于slot的理解

非静态方法或者构造方法中第一个变量是this
Slot的重复利用:
栈帧中的局部变量表中的槽位是可以重复利用的。如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

因为变量a只在大括号里面存在
静态变量与局部变量的对比
变量的分类:
按照数据类型分:
① 基本数据类型
② 引用数据类型
按照在类中声明的位置分:
① 成员变量:在使用前,都经历过默认初始化赋值。
a) 类(静态)变量
Linking的prepare阶段:给类变量赋默认值 ——> initial阶段:给类变量显式赋值
b) 实例变量
随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显示赋值,否则编译不通过

补充说明:
在栈帧中,与性能调优关系最密切的部分就是前面提到的局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递。(因为可能局部变量表中的变量是对堆空间中对象的引用)
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈(Operand Stack)
每一个独立的栈帧中除了局部变量表外,还包含一个后进先出的操作数栈,也可以称之为表达式栈(Expression Stack)。
操作数栈:在方法的执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。
举例:

操作数栈作用:
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随着被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的Code属性中,为max_stack的值。
栈中的任一个元素都可以是任意的java数据类型
32bit的类型占用一个单位的栈深度
64bit的类型占用两个单位的栈深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈/出栈操作来完成一次数据访问。

代码追踪(涉及操作数栈的字节码指令执行分析)
① 初始:将整数15压入操作数栈栈顶中

② 执行第二条指令:从栈顶中弹出整数15存入局部变量表中

③ 第三条指令:将整数8压倒栈顶

④ 第4条指令:将栈顶的整数8弹出,放到局部变量表中

⑤ 第5条指令:从局部变量表中第一个槽位取出整数15放入操作数栈中

⑥ 第6条指令:再将局部变量表中第二个槽位的整数8取出来,放到操作数栈中

⑦ 第7条指令:将操作数栈中的两个整数相加,得到值23,存储在栈顶

⑧ 第8条指令:将栈顶的值存入到局部变量表中第三个槽位

栈顶缓存(Top-of-Stack cashing)技术

动态链接(Dynamic Linking)的理解与常量池的使用
先明确一个概念:帧数据区:
包括方法返回地址部分、动态链接部分还有附加信息部分。

动态链接又被称为指向运行时常量池的方法引用:
每一个栈帧内部都包含一个指向 运行时常量池中 该栈帧所属方法的引用。包含该引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用 保存在class文件的常量池中。
比如,描述一个方法调用了另外的其他方法时,就是通过常量池(常量池在运行时的方法区中)中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

像#3、#6和#5就是符号引用。

为什么需要常量池?
常量池的作用:提供一些符号合常量,便于指令的识别。
比如说一个类中的属性应该放在该类的运行时常量池中,然后,用到它的方法的栈帧中保存一个引用即可,而不是在每个用到该属性的方法对应的栈帧中都保存一个,这样太浪费内存了。
方法的绑定机制:静态绑定与动态绑定
在JVM中,将符号引用转为为调用方法的直接引用与方法的绑定机制相关。
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期间可知,且运行期间保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接:
如果被调用的方法在编译期间无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定方法被称为晚期绑定。
对应的方法的绑定机制为:
早期绑定
晚期绑定
绑定是一个字段、方法或者类在 符号引用 被替换为 直接引用的过程,这仅发生一次。
早期绑定:
指被调用的目标方法如果在编译期间可知,且运行期间保持不变时,即可将这个方法与所属的类型进行绑定,如此,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定:
如果被调用的方法在编译期间无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定方法被称为晚期绑定。

方法调用指令区分:虚方法和非虚方法
非虚方法:
如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
使用非虚方法时,JVM确定每个对象都是调用的哪一个方法
子类对象的多态性的使用前提是:
①类的继承关系
②方法的重写

上图中蓝色标识的invokestatic和invokespecial两条指令都是非虚方法的调用指令。
其余的三条指令,除了用final修饰的外,都是虚方法的调用指令。
Invokedynamic指令的使用
首先区分动态类型语言和静态类型语言:
动态类型语言和静态类型语言两者的区别在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之就是动态类型语言。
直白的说:静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息(根据变量值来确定类型),变量没有类型信息,变量值才有类型信息。

方法重写的本质与虚方发表的使用
Java语言中方法重写的本质:

虚方法表:

虚方法表举例说明:
举例1:

子类Son继承自父类Father,当Father类或者Son类调用蓝色表示的方法时,因为它们都没有重写过这些方法,那么就直接调用虚方法表中指明的方法(也是Object类的方法)。
Father类定义了hardChoice(QQ)和hardChoice(_360)l两个方法,子类Son继承了Father类,并且重写了这两个方法,因此当Son类对象调用这两个方法时,对去查虚方法表,使用自己重写的方法。(重写的方法指向自己,没有重写的就指向父类(Object类))
举例2:

一个Friendly接口,一个Dog类
对于Dog类,它重写了toString方法,实现了sayHello方法,因此这两个方法在虚方法表中执行Dog类自己,而其他的方法指向Object类。
实现了Friendly接口并且继承了Dog类的CockerSpaniel类。
可卡犬类实现了Friendly接口,并重写了接口中的sayHello和sayGoodby方法,因此这两个方法在虚方法表中指向自己。虽然可卡犬类继承了Dog类,而Dog类中也有sayHello方法,但是因为自己重写了,所以不会调用父类的sayHello方法。另外,继承了Dog类,则同样继承了父类中的toString方法,因此在虚方法表中该方法指向Dog类。
Cat类实现了Friendly接口,并重写了sayHello和sayGoodbye方法,也重写了finalize和toString方法。
因此这四个方法和自己实现的eat方法都是指向自己的。.
方法返回地址(return address)的说明
方法返回地址是线程的虚拟机栈的栈帧中的一块内存区域。(就是当前方法执行完后应该跳转到哪里)
存放调用该方法的方法当时的PC寄存器的值。(比如方法A内部某一行调用了方法B)
一个方法的结束,有两种方式:
①正常执行完成
②出现未处理的异常,非正常退出
无论通过哪种方法退出,在方法退出后都返回到该方法被调用的位置。
方法正常退出时,调用者的pc计数器的值作为返回地址,即调用方法的指令的下一条指令的地址。
而通过异常退出的,返回地址要通过异常表(Exception Table)来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器等,让调用者方法继续执行下去。
正常完成出口与异常完成出口的区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何的返回值。
栈帧中的一些附加信息
该部分信息是可选的。可以携带一些与java虚拟机实现相关的一些附加信息。
比如,对程序调试提供支持的信息。
虚拟机栈的5道面试题
-
举例栈溢出的情况(StackOverflowError)
通过-Xss来设置栈的大小 -
调整栈的大小,就能保证不溢出吗?
不能保证。可能只是延缓了栈溢出发生的时间。 -
分配的栈内存越大越好吗?
No! -
垃圾回收是否涉及到虚拟机栈?
不会!
| 各区域 |Error(StackOverflow,oom等)| GC |
|----------------|-------------|-------|
| 程序计数器 | × | × |
| 本地方法栈 | √ | × |
| 虚拟机栈 | √ | × |
| 堆 | √ | √ |
| 方法区 | √ | √ | -
方法中定义的局部变量是否线程安全?
具体问题具体分析。
如果该局部变量就仅仅是在方法内部存在,那么就是线程安全的。
一旦该变量有可能被其他线程访问到,就可能出现不安全情况。

浙公网安备 33010602011771号