基于栈的设计
对于数据的操作,有基于cpu寄存器的设计和基于栈的设计两种。jvm为何选择了基于栈的设计呢?
如果是基于cpu寄存器的设计,则不得不依赖底层的操作系统,这样就无法满足跨平台的特性。为了满足跨平台,基于栈的设计相比第一种(基于cpu寄存器的架构)性能是有所损失的。
虚拟机栈异常
java虚拟机规范中对于虚拟机栈的错误定义了两种:StackOverflowError和OutOfMemoryError。栈如果设置的固定大小,当栈的深度超过了设置的大小,即抛第一种错误异常,一般发生在递归调用或死循环中。栈如果设置的动态可扩展,当栈深过深会申请内存,内存申请不到,即抛第二种错误异常。
栈帧
虚拟机栈是线程私有的,基本单位是栈帧(stack frame),由以下4个部分组成:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
虚拟机栈只有入栈和出栈两种操作,jvm对于虚拟机栈没有垃圾回收操作。入栈相当于java代码中的方法调用,出栈相当于方法调用的结束。
局部变量表
虚拟机栈中有一个或多个栈帧组成,而每个栈帧中都包含局部变量表。
局部变量表本质上的数据结构是数组,其大小是在编译的时候就决定了,并且在运行期间不会改变(在idea中可以使用jclasslib插件查看,找到.class文件,View > Show Bytecode With Jclasslib)。
局部变量表用于存储方法的入参,定义在方法中的基本数据类型,引用类型以及方法返回的地址。
局部变量表基本单位是slot,其中long和double占用了两个连续基本单位(在取数时使用下标小的取)。slot基本单位是可以重复利用的,如果一个本地变量在操作后不再使用,它在局部变量表中的位置可以被后续定义的变量替换。
操作数栈
每个栈帧中也都包含一个操作数栈,其也是用数组实现的,其深度也在编译器的时候就决定了。
在每个栈帧刚开始创建的时候,操作数栈数组是空的。和局部变量表一样,long和double占用了两个单位的深度,其他基本类型只占用一个。
操作数栈也只有入栈和出栈两种行为,操作数栈将局部变量,字段中的常量或者值压入栈中,供jvm的其他指令取出(即出栈)进行操作(比如加减乘除),然后将结果再次入栈。对于有返回值的方法,操作数栈会将最终的执行结果压入栈顶,传递给调用方。
动态链接
每个栈帧都包含了运行时常量池的引用(每个class文件都有一份常量池,记录了class中定义的方法和变量的符号引用,并且在class加载后放到运行时常量池中),动态链接用于在解析时将符号引用替换成直接的地址(关于符号引用需要看.class的字节码)。
静态链接(早期绑定),当一个字节码文件被加载进jvm内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接(晚期绑定),如果被调用的方法在编译期间无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
方法返回地址
java虚拟机规范中,只规定了正常的方法返回(Normal Method Invocation Completion)和异常的方法返回(Abrupt Method Invocation Completion)两种。
如果方法是正常返回的,存放该方法调用处的PC寄存器的值。
如果方法是异常退出的,则通过异常表进行处理。