目录

  • 出现背景
  • 概括
  • 异常
  • 优点
  • 栈帧
  • 内部结构
  • 局部变量表
  • slot
  • 操作数栈
  • 栈顶缓存
  • 动态链接
  • 方法的调用
  • 虚方法和非虚方法
  • 虚方法表
  • 方法返回地址
  • 一些附加信息

出现背景

由于Java是跨平台的,不同平台CPU的架构不同,不能设计为基于寄存器,所以Java的指令都是根据栈来设计的。

概括

每个 Java 虚拟机线程都有一个私有的Java 虚拟机栈,与线程同时创建。Java 虚拟机栈存储栈帧,它保存局部变量和部分结果,并在方法调用和返回中发挥作用。Java 虚拟机栈具有固定大小或根据计算需要动态扩展和收缩。

异常

  1. 如果线程请求分配的栈容量超过Java虚拟机栈的最大容量,则 Java 虚拟机会抛出一个StackOverflowError.
  2. 如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展但没有足够的内存来实现扩展,或者如果没有足够的内存可以为新线程创建初始 Java 虚拟机栈,则 Java 虚拟机抛出一个OutOfMemoryError.

优点

栈只有两个操作:进栈、出栈,对于栈不存在垃圾回收。

栈帧

  1. 每个线程都有自己的栈,栈中的数据都是以栈帧(stack Frame)的格式存在。
  2. A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.
  3. 每次调用方法时都会创建一个栈帧

内部结构

每个栈帧都有局部变量表、操作数堆栈以及对当前方法类的运行时常量池的引用、方法返回地址、一些附加信息

局部变量表

  1. 局部变量表也被称之为局部变量数组或本地变量表
  2. 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
  3. 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  4. 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  5. 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
  6. 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
  7. 非静态方法的局部变量表第一个变量是 this
public void hello() {
    String name = "hhh";
  }

将代码编译后,通过jclasslib打开

java 虚拟机栈存储在堆区吗 java虚拟机栈动态扩展_虚方法

java 虚拟机栈存储在堆区吗 java虚拟机栈动态扩展_局部变量_02

slot

  1. 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
  2. 局部变量表,最基本的存储单元是slot(变量槽)
  3. 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型( reference) ,returnAddress类型
  4. A single local variable can hold a value of type boolean, byte, char, short, int, float, reference, or returnAddress. A pair of local variables can hold a value of type long or double.
    在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
    4.1 byte . short . char在存储前被转换为int, bpolean也被转换为int,o表示false ,非o表示true。
    4.2 long和double 占据两个slot。
  5. 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
  6. 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
  7. 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
  8. 如果一个变量过了其作用域,那么在其作用域之后声明的变量可能会用这个局部变量的槽位

操作数栈

  1. 每个帧都包含一个后进先出 (LIFO) 堆栈,称为其操作数堆栈。帧的操作数堆栈的最大深度在编译时确定。
  2. 创建栈帧时,操作数堆栈为空。Java 虚拟机提供将常量或值从局部变量或字段加载到操作数堆栈的指令。其他 Java 虚拟机指令从操作数堆栈中获取操作数,对它们进行操作,然后将结果推回到操作数堆栈上。
  3. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。
  4. Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

栈顶缓存

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理cPu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接

Each frame contains a reference to the run-time constant poolfor the type of the current method to support dynamic linking of the method code. The class file code for a method refers to methods to be invoked and variables to be accessed via symbolic references. Dynamic linking translates these symbolic method references into concrete method references, loading classes as necessary to resolve as-yet-undefined symbols, and translates variable accesses into appropriate offsets in storage structures associated with the run-time location of these variables.

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

方法的调用

  1. 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
    1.1 静态链接:目标方法在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用转化为可确定的直接引用
    1.2 动态链接:目标方法在编译期间无法被确定,只有在程序运行期间才能将调用方法的符号引用转化为直接引用

虚方法和非虚方法

  1. 如果方法在编译期就确定了具体的调用版木,这个版木在运行时是不可变的。这样的方法称为非虚方法。
  2. 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  3. 其他方法为虚方法

虚方法表

  1. 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表( virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
  2. 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  3. 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

方法返回地址

  1. 存放调用该方法的PC寄存器的值
  2. 无论方法通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
  3. 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值.

一些附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。

  1. 举例栈溢出的情况?
    StackOverflowError
  2. 调整栈大小,能保证不出现溢出吗?
    不能
  3. 分配的栈内存越大越好吗?
    不是
  4. 垃圾回收是否会涉及到虚拟机栈?
    不会
  5. 方法中定义的局部变量是否线程安全?
    具体问题具体分析
public static StringBuilder method() {
    //线程不安全
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("a");
    stringBuilder.append("b");
    return stringBuilder;
  }
public static String method() {
    //线程安全
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("a");
    stringBuilder.append("b");
    return stringBuilder.toString();
  }