JVM(八) - 虚拟机栈中的栈帧

Jvm内存中栈分两种:本地方法栈 和 虚拟机栈。两者没有本质上的区别,区别在于服务的对象不同。本地方法栈服务于JVM虚拟机的native方法,如JDK安装目录下很多C的文件实现的方法,这些就是native方法。虚拟机栈服务于虚拟机执行的Java方法。

一、栈帧

栈帧其实就是栈里面的元素,用于支持Java虚拟机进行方法调用和方法执行背后的数据结构。了解它就可以更好地理解Java虚拟机执行引擎是如何运行的。

  • 一个线程对应一个虚拟机栈,当前CPU调度的那个线程叫做活动线程。
  • 每个方法被执行时会创建一个栈帧(Stack Frame),进入虚拟机栈,一个栈帧对应一个方法
  • 每个方法结束对应着栈帧的出栈,入栈表示被调用,出栈表示执行完毕或者返回异常;
  • 栈帧是一个数据结构,所以用于存储局部变量表(Local Variable)、操作数栈(Operand Stack)、动态链接(Dynamic Link)、方法出口(Return Address)等信息
  • 活动线程的虚拟机栈里最顶部的栈帧代表当前正在执行的方法,这个栈帧也被叫做"当前栈帧"。
  • 在同一时刻、同一条线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

虚拟机栈和栈帧的结构如图:

JVM(八) - 虚拟机栈中的栈帧_第1张图片

 栈帧的部分信息,如局部变量表、操作数栈、运行时常量池可以通过JVM字节码查看:

javap -v 类名

public class ShowJOL {
    public static volatile int a = 3;

    public static void main(String[] args) {
        a  =  5;
    }
}

// 执行javap -p ShowJOL
Classfile /Users/shaotuo/java/java/target/classes/edward/com/ShowJOL.class
  Last modified 2022-3-15; size 474 bytes
  MD5 checksum 19d688ea832ca46ef13267208f9d22e1
  Compiled from "ShowJOL.java"
public class edward.com.ShowJOL
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#21         // java/lang/Object."":()V
   #2 = Fieldref           #3.#22         // edward/com/ShowJOL.a:I
   #3 = Class              #23            // edward/com/ShowJOL
   #4 = Class              #24            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Ledward/com/ShowJOL;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               
  #19 = Utf8               SourceFile
  #20 = Utf8               ShowJOL.java
  #21 = NameAndType        #7:#8          // "":()V
  #22 = NameAndType        #5:#6          // a:I
  #23 = Utf8               edward/com/ShowJOL
  #24 = Utf8               java/lang/Object
{
  public static volatile int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

  public edward.com.ShowJOL();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ledward/com/ShowJOL;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_5
         1: putstatic     #2                  // Field a:I
         4: return
      LineNumberTable:
        line 7: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_3
         1: putstatic     #2                  // Field a:I
         4: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "ShowJOL.java"

二、局部变量表(Local Variable)

局部变量表也叫本地变量表,用于存放方法参数方法内部定义的局部变量的数据结构。

局部变量表的存储

局部变量表一片逻辑连续的内存空间,最小单位是变量槽(Variable Slot),每个槽的大小为32位

  • 可以存放基本boolean、byte、char、short、int、float和reference这几种类型。
  • reference类型是一个对象实例的引用,根据引用直接或间接地查找到实例在Java堆中的数据存放的起始地或索引;
  • reference在局部变量表存放的是对象在堆中的地址,所以栈里面有很多指针指向堆;
  • long和double这两种类型是64位数据类型,以高位对齐的方式分配两个连续槽存储
  • 如果在实例的方法(非static的方法)的调用栈中,第0位槽默认就是该对象实例的引用,即方法中的this
  • 其余参数则按照参数表顺序排列,再顺序排列方法体内的局部变量顺序和作用域分配其余的变量槽。

局部变量表的使用

虚拟机通过索引定位对应数据,索引值范围是从0到局部变量表最大槽数量-1。

  • 成员方法(非static的方法),则第0位索引即方法中的this
  • 访问32位数据类型的变量,索引N即代表了使用第N个变量槽;
  • 访问64位数据类型的变量,需要同时使用第N和N+1两个变量槽;

局部变量表的Reference

对象的使用就涉及到了对象的引用,在Java程序操作堆上对象需要通过Java栈上的reference数据;而reference类型在Java虚拟机规范里面只规定了是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置。所以对象访问方式也是取决于虚拟机实现而定的,主流的访问方式有使用句柄直接指针两种。

1、使用句柄访问对象

该方式中,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图

JVM(八) - 虚拟机栈中的栈帧_第2张图片

好处:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

2、使用直接指针访问对象

Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图: JVM(八) - 虚拟机栈中的栈帧_第3张图片

 好处:速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。就虚拟机HotSpot而言,它是使用直接指针方式进行对象访问。

三、操作数栈(Operand Stack)

供方法执行中操作数进行运算的临时存储空间,为栈结构

  • 每个栈帧都包含一个操作数栈,是后进先出的栈;
  • 栈桢刚创建时(方法执行时),里面的操作数栈是空的;
  • 虚拟机通过指令让一些数据进行入栈和出栈操作,如局部变量表里的数据、实例的字段等数据入栈。如节码指令iadd执行,将栈顶已经存入了两个int型的数值相加,然后将相加的结果重新入栈。iadd指令中,只能用于整型数的加法,所以栈顶的两个元素的必须为int型,数据类型必须与字节码指令的序列严格匹配,在编译代码时,编译器会严格保证这一点,在类加载的校验阶段也会再次验证这一点。
  • 一个方法调用另外一个方法时,向其他方法传参的参数,也存在操作数栈中;
  • 其他方法返回的结果,返回时存在操作数栈中;

栈桢和栈桢是完全独立的吗?

栈桢作为虚拟机栈的一个单元,应该是栈桢之间完全独立的。

但是虚拟机进行了一些优化:为了节省方法内调用方法间参数、返回值的复制传递等一些操作,就让一部分数据进行栈桢间共享。如下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,共用数据,节约空间,减少操作如下图:

JVM(八) - 虚拟机栈中的栈帧_第4张图片

在已编译Class类文件中,就已经计算好操作数栈所需要分配的内存,存储在类文件中方法Code属性的stack数据项中。

四、动态链接(Dynamic Link)

在方法执行期间,将常量池中的符号引用动态的转化为直接引用(实际的内存地址)

1、链接

Class类文件的常量池中存有大量的符号引用,如一个方法内调用另一个方法this.testMethod(),或一个类使用另一个类的成员变量b.testVar,其中testMethod/testVar就是指符号,在运行时会将这些符号转化为对应变量或方法存储在元数据空间的内存地址或入口,简单理解就是动态链接存储的就是这些地址。而这些符号的解析有两种场景:

  • 静态解析:在类加载阶段或者第一次使用的时就被转化为直接引用(实际运行时内存布局中的入口地址)
  • 动态连接:将在每一次运行期间转化为直接引用

2、绑定机制

JVM将符号引用转换为调用方法的直接引用与方法的绑定机制相关,绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,分为早期绑定(Early Binding)和晚期绑定(Late Binding)。

  • 早期绑定:被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定:被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定,即动态链接。

3、代码例子

 

/*
* 说明早期绑定和晚期绑定的例子
*/
class Animal {
    public void eat() {
        System.out.println("动物进食");
    }
}
 
interface Huntable {
    void hunt();
}
 
class Dog extends Animal implements Huntable {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
 
    @Override
    public void hunt() {
        System.out.println("捕食耗子,多管闲事");
    }
}
 
class Cat extends Animal implements Huntable {
    public Cat() {
        super(); // 表现为:早期绑定
    }
 
    public Cat(String name) {
        this(); // 表现为:早期绑定
    }
 
    @Override
    public void eat() {
        super.eat(); // 表现为:早期绑定
        System.out.println("猫吃鱼");
    }
 
    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
 
public class AnimalTest {
    public void showAnimal(Animal animal) {
        animal.eat(); // 表现为:晚期绑定
    }
 
 
    public void showHunt(Huntable h) {
        h.hunt(); // 表现为:晚期绑定
    }
}

晚期绑定:

1、调用父类方法,invokevirtual:调用实例方法

public void showAnimal(Animal animal) {    
    animal.eat(); // 表现为:晚期绑定
}
// 对应字节码
invokevirtual #2 

2、调用接口方法,invokeinterface:调用接口方法

public void showHunt(Huntable h) {
    h.hunt(); // 表现为:晚期绑定
}
// 对应字节码
invokeinterface #3  count 1

早期绑定:

invokespecial,在下面三种情况下使用:

  • the instance initialization method,
  • a private method of this
  • a method in a superclass of this

动态链接的前提?

每个栈帧都包含一个指向运行时常量池(位于方法区)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

五、方法出口(Return Address)

方法出口保存方法调用的返回信息在栈帧中,如方法中掉其他方法,用来于恢复调用者(调用其他方法的当前方法)的执行状态。

  • 方法退出等同于当前栈帧出栈。
  • 方法正常退出时,有返回值时将返回值传递给调用者;
  • 方法异常退出时,返回信息是要通过异常处理器表来确定的,不会给上层调用者任何返回值;

方法退出时,可能恢复调用者的那些执行状态?

  • 局部变量表、操作数栈 和 程序计数器(pc指针);
  • 程序计数器要适当地增加,来指向下一条指令;
  • 有返回值时应push到调用者的操作数栈中;

你可能感兴趣的:(JVM系列,jvm,java,开发语言)