现在的位置: 首页 > 综合 > 正文

JVM栈

2018年02月03日 ⁄ 综合 ⁄ 共 3511字 ⁄ 字号 评论关闭
Java中的栈

每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。某个线程正在执行的方法称为当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。当线程执行一个方法时,它会跟踪当前常量池。
每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,这个帧自然就成了当前帧。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等。
Java栈上的所有数据都是私有的。任何线程都不能访问另一个线程的栈数据。所以我们不用考虑多线程情况下栈数据访问同步的情况。
像方法区和堆一样,Java栈和帧在内存中也不必是连续的,帧可以分布在连续的栈里,也可以分布在堆里

Java栈的组成元素——栈帧

栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在 Java 虚拟机栈之中,每一个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。局部变量表和操作数栈的容量是在编译期确定,并通过方法的 Code 属性保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于 Java 虚拟机的实现和方法调用时可被分配的内存。
在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。
请读者特别注意,栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。

局部变量表

每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code 属性保存及提供给栈帧使用。
一个局部变量可以保存一个类型为 boolean、byte、char、short、float、reference 和 returnAddress 的数据,两个局部变量可以保存一个类型为 long 和 double 的数据。
局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。

long 和 double 类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。例如我们讲一个 double 类型的值存储在索引值为 n 的局部变量中,实际上的意思是索引值为 n 和 n+1 的两个局部变量都用来存储这个值。索引值为 n+1 的局部变量是无法直接读取的,但是可能会被写入,不过如果进行了这种操作,就将会导致局部变量 n 的内容失效掉。

上文中提及的局部变量 n 的 n 值并不要求一定是偶数,Java 虚拟机也不要求 double 和 long 类型数据采用 64 位对其的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个 double 或 long 类型的值。

Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从 0 开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“this”关键字)。后续的其他参数将会传递至从 1 开始的连续的局部变量表位置上。

操作数栈

每一个栈帧(§2.6)内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的 Code 属性保存及提供给栈帧使用。
在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。
操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java 虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

举个例子,iadd 字节码指令的作用是将两个 int 类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的 int 型数值。在 iadd 指令执行时,2 个 int 值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。

每一个操作数栈的成员(Entry)可以保存一个 Java 虚拟机中定义的任意数据类型的值,包括 long 和 double 类型。在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈栈顶的数据类型相匹配,例如不可以入栈两个 int 类型的数据,然后当作 long 类型去操作他们,或者入栈两个 float 类型的数据,然后使用 iadd 指令去对它们进行求和。有一小部分 Java 虚拟机指令(例如 dup 和
swap 指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过 Class 文件的校验过程(§4.10)来强制保障。

在任意时刻,操作数栈都会有一个确定的栈深度,一个 long 或者 double 类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。

动态链接

每一个栈帧内部都包含一个指向运行时常量池(§2.5.5)的引用来支持当前方法的代码实现动态链接(Dynamic Linking)。在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。

由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响。

方法正常调用完成

方法正常调用完成是指在方法的执行过程中,没有任何异常被抛出——包括直接从 Java 虚拟机之中抛出的异常以及在执行时通过 throw 语句显式抛出的异常。如果当前方法调用正常完成的话,它很可能会返回一个值给调用它的方法,方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令的时候,使用哪种返回指令取决于方法返回值的数据类型(如果有返回值的话)。

在这种场景下,当前栈帧承担着回复调用者状态的责任,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行。

方法异常调用完成

方法异常调用完成是指在方法的执行过程中,某些指令导致了 Java 虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到了 athrow 字节码指令显式地抛出异常,并且在该方法内部没有把异常捕获住。如果方法异常调用完成,那一定不会有方法返回值返回给它的调用者。

抱歉!评论已关闭.