虚拟机栈:由于Java跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。所以指令集小,编译器容易实现,但是性能下降,实现同样的功能需要更多的指令。
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。设置栈空间大小:可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
虚拟机栈特点:栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器;JVM直接对Java虚拟机栈的操作只有两个【每个方法执行,伴随着进栈和出栈】;对于栈来说不存在垃圾回收问题,但存在异常问题。
栈中常见的异常:
- 栈溢出异常StackOverflowError:如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个栈溢出异常StackOverflowError异常。
-
超出内存容量异常OutOfMemoryError:如果Java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出一个超出内存容量异常OutOfMemoryError异常。
栈的基本单位栈帧
-
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
-
栈帧的内部结构:局部变量表、操作数帧、动态链接、方法返回地址、一些附加信息。
栈的运行原理:JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出原则;正在执行的线程,一个时间点上,只有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果当前方法中调用了其它方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
1 局部变量表
局部变量表本质是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用、返回值类型。
- 由于局部变量是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
- 局部变量表的容量大小是在编译期确定下来的,并保存方法的Code属性的maximum、local、variables数据项中,在方法运行器件不会改变局部变量表的大小。
- 局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多;方法中的参数和局部变量越多,局部变量表越大,栈帧就越大,栈帧嵌套的次数就会越少。
- 局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
- 局部变量表是性能调优最为关心的部分,局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
局部变量表的基本单位变量槽slot:
-
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,局部变量表最基本的存储单元是slot(变量槽)。JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引可以成功访问到局部变量表中指定的局部变量值。
-
局部变量中存放编译期间可知的各种基本数据类型、对象引用、返回值类型的局部变量,在局部变量表中,32位以内的类型只占用一个slot(包括对象引用、返回值类型),64位的类型(long、double)占用两个slot。局部变量表中存放局部变量,所有类型都会转换为int类型。
-
实例方法的参数和方法体内部定义的局部变量将会被按照定义顺序复制到局部变量表中的每一个slot上,this变量放在第一个位置上。静态方法的局部变量表中不存在this变量,所以静态方法不能访问this。
-
栈帧中的局部变量表中的槽位是可以复用的,如果一个局部变量过来其作用域,那么在其作用域之后申明的新的局部变量很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
局部变量是线程安全的吗
所有的基本数据类型局部变量都是线程安全的,当方法传参时传递的是变量值,不是变量。在方法体中定义的并且在方法体中消亡的引用数据类型变量是线程安全的,无法被其它线程使用。在方法体中定义的但是被返回出去了的引用数据类型变量是线程不安全的,可能被接收到传递给其它线程使用。在方法参数中接收到引用类型实参的引用数据类型变量是线程不安全的,可能在被调用处传递给其它线程使用。
2 操作数栈
操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧会随之创建出来,这个方法的操作数栈最开始是空的。操作数栈实际上是一个数组,操作数栈的大小在编译期间就确定下来了,保存在方法的Code属性中,为max-stack的值。
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量历史的存储空间,在方法的执行过程中,根据字节码指令,往栈中写入数据或提取数据(入栈或出栈)。操作数栈并非采用索引的方式来进行数据访问,而是通过标准的入栈和出栈操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,32位的类型占用一个栈单位深度,64位的类型占用两个栈单位深度。
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈,执行引擎根据PC寄存器指向的操作数去执行该操作数。
3 动态链接
动态链接的作用是将符号引用转换为调用方法的直接引用,在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的。
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态连接(Dynamic Linking)。比如iinvokedynamic指令。
4 方法返回值
方法返回值存储该方法的PC寄存器的值,当方法正常执行结束退出时,需要通过方法返回值修改PC寄存器来指向被调用的位置。但是通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
方法的结束方式:正常执行结束;出现未处理的异常,非正常退出
-
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者。在字节码指令中,返回指令包含ireturn(当返回值类型是boolean、byte、char、short、int类型)、lreturn(当返回值类型为long)、freturn(当返回值类型为float)、dreturn(当返回值为double)、areturn(当返回值类型为引用类型)、return(当返回值为void类型、构造器)
-
异常完成出口:在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在笨方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。方法执行过程中抛出异常时的异常处理,会存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
5 一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
参考:尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili