虚拟机栈
- 虚拟机栈是运行时数据区的另一个区域,同时虚拟机栈也是运行时数据区的另一个线程私有区域
- 由于跨平台的设计,Java的指令都是根据栈来设计的,不同平台的CPU架构不同,所以不能设计为基于寄存器的
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
- 栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者如何处理数据.堆解决的是数据的存储问题,即数据放哪怎么放
- 每个线程创建时都会创建一个虚拟机栈,其内部保存着一个个栈帧,对应着一次次Java方法的调用
- 生命周期: 生命周期和线程一致
- 作用:主管java程序的运行,他保存方法的局部变量(八种基本数据类型,对象的引用地址)、部分结果、并参与方法的调用和返回
- 局部变量 成员变量(属性)
- 基本数据变量 引用类型变量(类、数组、接口)
栈的优点
- 栈是一种快速有效的分配存储方式,访问速度仅此于程序计数器
- JVM直接对Java栈的操作只有两个
- 每个方法执行,伴随着入栈、出栈
- 执行结束后的出栈工作
- 对栈来说,不存在垃圾回收问题
- 栈中可能出现的异常
- Java虚拟机规范中允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的JAVA虚拟机栈,那每一个线程的Java虚拟机栈容量可以再线程创建的时候独立选择,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
- 如果虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常
- 设置栈的大小
- 我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
- 演示栈中的溢出,写没有出口的递归调用,通过-Xss设置栈帧大小.
public class StackError {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count ++;
main(args);
}
}
栈的存储单位
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循"先进后出"/"后进先出"原则
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧.即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的.这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对于当前栈帧进行操作
- 如果在该方法中调用其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前帧
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test.method1();
}
public void method1(){
System.out.println("method1()执行....");
method2();
System.out.println("method1()执行结束");
}
public int method2(){
System.out.println("method2()开始执行");
int i = 10;
int m = (int) method3();
System.out.println("method2()即将结束");
return i+m;
}
private double method3() {
System.out.println("method3()开始执行");
double j = 20.0;
System.out.println("method3()即将结束");
return j;
}
}
- 不同的线程中所包含的栈帧是不允许存在相互引用的,即不肯能在一个栈帧之中引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令, 另外一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部结构
- 每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
局部变量表
- 局部变量表也被称为局部变量数组或者本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法内部的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及rerurnAddress类型
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需容量大小是在编译器确定下来的,并保存在方法的Cod属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表大小的
- 方法嵌套调用的次数由栈的大小决定. 一般来说 ,栈越大,方法嵌套调用次数越多. 对于一个函数来言,他的参数和局部变量越多,使得局部变量表膨胀,他的栈帧就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
- 局部变量表中的变量只在当前方法调用中有效.在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程.当方法调用结束后,锁着方法栈帧的销毁,局部变量表也会随之销毁
public class LocalVariablesTest {
public int count = 0;
public static void testStatic(){
LocalVariablesTest localVariablesTest = new LocalVariablesTest();
Date date = new Date();
int count = 10;
System.out.println(count);
}
public void test1(){
Date date = new Date();
String name1 = "aiyouwei";
String info = test2(date,name1);
System.out.println(date+name1);
}
public String test2(Date dateP,String name2){
dateP = null;
name2 = "weijianglei";
double weight = 120.5;
char gender = '男';
return dateP+name2;
}
public void test3(){
this.count++;
}
public void test4(){
int a = 0;
{
int b = 0;
b = a + 1;
}
int c = a+1;
}
}
操作数栈
- 栈:可以使用数组或链表来实现
- 每一个独立的栈帧中除了包含局部变量表意外,还包含一个后进先出的操作数栈,也可以成为表达式栈
- 操作数栈,在方法执行过程中,根据字节码指令,往栈帧中写入数据或提取数据,即入栈(push)/出栈(pop)
- 某些字节码指令将值压入操作数栈,其余字节码指令将操作数取出栈,使用它们后再把结果压入栈
- 比如:执行复制、交换、求和等操作
- 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会被随之创建出来 ,这个方法的操作数栈是空的
- 每一个操作数栈都会拥有一个明确的栈深度用来存储数值,其中所需最大的深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值
- 栈中的任何一个元素都是可以是任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中, 并更新PC寄存器中下一条需要执行的字节码指令
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
动态链接(或指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 比如:invikedynamic指令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都做为符号引用(Symcolic Reference)保存在class文件的常量池里.比如,描述一个方法调用了另外其他的方法
- 如下图: #x 代表的为指向的符号引用
public class DynamicLinkingTest {
int num = 10;
public void methodA(){
System.out.println("methodB");
}
public void methodB(){
System.out.println("methodB");
methodA();
num++;
}
}
- 常量池的作用:就是为了提供一些符号和常量,便于指令识别
方法返回地址(Return Address)
- 存放调用该方法的PC寄存器的值
- 一个方法正常执行结束,有两种方式
- 正常执行结束(执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口)
- 一个方法在正常调用完成后究竟需要哪一个返回指令还需要根据方法返回值的实际数据类型而定
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(long),freturn(float),dreturn(double)以及areturn(引用类型),另外还有一个return指令供声明为void的方法,实例初始化方法,类和接口的初始化方法使用
- 出现未处理异常,非正常退出,在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内部进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出.简称异常完成出口.方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的方法
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置.方法正常退出时**,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址**.而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息
- 本质上,方法的退出就是当前栈帧出栈的过程,此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用这方法继续执行下去
- 正常的完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
public boolean methodBoolean(){
return false;
}
public byte methodByte(){
return 0;
}
public short methodShort(){
return 0;
}
public char methodChar(){
return 'a';
}
public int methodInt(){
return 0;
}
public long methodLong(){
return 0L;
}
public float methodFloat(){
return 0.0f;
}
public double methodDouble(){
return 0.0;
}
public String methodString(){
return null;
}
public Date methodDate(){
return new Date();
}
public void methodVoid(){
}
### 一些附加信息
- 栈帧中还允许携带与Java虚拟机实现相关的一些附加信息.例如,对对程序调试提供支持的信息
|