0032-虚拟机栈

文章目录

      • 1 简介
      • 2 栈配置
      • 3 栈帧
        • 3.1 局部变量表(local variables)
        • 3.2 操作数栈(Operand Stack)
        • 3.3 动态链接(Dynamic Linking)
        • 3.4 方法返回地址(Return Address)
        • 3.5 一些附加信息
      • 4. 栈的相关面试题

1 简介

java虚拟机栈,早期也称为java栈,每个线程在创建时,都会创建一个虚拟机栈,其内部包含一个个栈帧(Stack Frame),对应一次次的方法调用,虚拟机栈是线程私有的,没有GC,有可能会出现StackOverFlowError或OutOfMemoryError

2 栈配置

# 默认是1m,-XX:+PrintFlagsFinal可以打印最终配置,-XX:+PrintFlagsInitial可以打印初始默认配置
-Xss2m

栈的大小会决定方法调用的深度,也即能存储多少个栈帧

3 栈帧

栈帧内容

  1. 局部变量表(Local Variables)

  2. 操作数栈(Operand Stack)

  3. 动态链接(Dynamic Linking)(指向运行时常量池的方法引用)

  4. 方法返回地址(Return)

  5. 一些附加信息

3.1 局部变量表(local variables)

1. 简介

  1. 局部变量表是一个数字数组,主要用于存储方法参数
    和定义在方法体内的局部变量,包括基本数据类型,对象引用,以及returnAddress类型

  2. 局部变量建立在线程的栈上,是线程的私有数据,不存在线程安全问题

  3. 局部变量表所需的容量大小是在编译期确定下来的,保存在方法的code属性的maximum local variables数据项中,
    运行时不会改变局部变量表的大小

  4. 栈帧的的大小受局部变量表影响,局部变量越多,栈帧越大,方法可以嵌套的次数越少

  5. 局部变量数组,最基本的单位是Slot(变量槽)

  6. 在局部变量表里32位以内的类型只占用一个slot(包括returnAddress类型)
    64位的类型(long和double)占用两个slot
    (byte,short,char在存储前被转换为int,boolean也被转换为int,0为false,非0表示true)

  7. 局部变量表为每一个slot分配一个访问索引,通过这个索引可以访问局部变量值
    7.1 如果需要访问64位的局部变量值时,只需要使用前一个索引即可
    7.2 如果当前栈帧是由构造方法或者实例方法创建,该对象应用this会放在index为0的slot处

  8. 栈帧中的局部变量表中的槽位可以重用,如果一个局部变量过了作用域,后面声明的局部变量可以覆盖这个slot

2. 静态变量与局部变量的对比

  1. 静态变量有两次初始化的机会,一次在“准备阶段”的零值初始化,另一次在“初始化阶段”实际赋值

  2. 局部变量没有零值初始化的过程,所以创建必须赋值,否则无法使用

3.2 操作数栈(Operand Stack)

1. 简述

  1. 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量零时的存储空间

  2. 操作数栈是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

  3. 每个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了
    保存在方法Code属性中,为max_stack的值

  4. 栈中的任何一个元素都是可以任意的java数据类型
    4.1 32bit的类型占用一个栈单位深度
    4.2 64bit的类型占用两个栈单位深度

  5. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令

  6. java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

2. 代码追踪


0032-虚拟机栈_第1张图片
0032-虚拟机栈_第2张图片
0032-虚拟机栈_第3张图片
0032-虚拟机栈_第4张图片
0032-虚拟机栈_第5张图片

3.3 动态链接(Dynamic Linking)

1. 简述

  1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。
    包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)

  2. 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用,保存在class文件的常量池里。
    比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

  3. 这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。 另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

0032-虚拟机栈_第6张图片
2. 方法调用指令
在class字节码码中,不同的方法有不同的指令

  • 普通调用指令
  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本

  2. invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本

  3. invokevirtual:调用所有虚方法

  4. invokeinterface:调用接口方法

  • 动态调用指令
  1. invokedynamic:动态解析出需要调用的方法,然后执行

由invokestatic指令和invokespecial指令调用的方法称为非虚方法(便宜期就确定了具体的调用版本,运行时不可变的方法),其余的(final修饰的除外),称为虚方法

3. 方法重写的本质

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C

  2. 如果在类型C中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,
    如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常

  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程

  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

4. 虚方法表

  1. 在面向对象的编程中,会很频繁的使用动态分派,如果每次动态分派都按上述步骤搜索,会影响执行效率
    为了提高性能,jvm采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找

  2. 每个类都有一个虚方法表,存在各个方法的实际入口

  3. 虚方法表在类加载的链接阶段被创建并开始初始化

3.4 方法返回地址(Return Address)

  1. 一个方法的结束,有两种方式:
    1.1 正常执行完成
    1.2 出现未处理的异常,非正常退出

  2. 方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法指令的下一条指令地址;
    通过异常退出的,需要根据异常表确定下一条指令地址(字节码指令中可以看到,异常表)

3.5 一些附加信息

4. 栈的相关面试题

  1. 举例栈溢出的情况?(StackOverFlowError)

通过-Xss设置栈的大小

  1. 调整栈的大小就能保证栈不出现溢出吗?

不能

  1. 垃圾回收是否会涉及到虚拟机栈?

不会

  1. 方法中定义的局部变量是否线程安全?

没有发生逃逸是线程安全的,否则就不安全

你可能感兴趣的:(#,Java虚拟机,jvm,虚拟机栈)