JVM中栈帧的详细讲解-刘宇

作者:刘宇
CSDN博客地址:https://blog.csdn.net/liuyu973971883
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。

一、什么是栈帧

  • 栈帧是栈中的一个栈元素,是一中用于帮助虚拟机执行方法调用与方法执行的数据结构,当前线程中,每执行一个方法就会往栈中插入一个栈帧。
  • 栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法返回地址(即返回到方法的调用者)以及操作数栈
  • Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,换句话说,每个线程都会有一个栈,所以对于栈帧来说不存在并发调用的情况。
    在这里插入图片描述

二、栈帧的组成部分

2.1、动态链接

  • 假设A类调用了B类的一个方法,在程序编译期间A对B的这种方法调用之间的地址关系是无法确定的,只有在类加载以及运行期间去调用的时候才能确定地址,由此就引入了符号引用直接引用,那么动态链接就是存储他们直接的关系的
  • Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。而转换成直接引用会分静态解析、动态连接两个部分。
  • 静态解析:在类加载阶段或者第一次使用的时候这些符号引用一部分就会转化为直接引用。静态解析的4中情形:1、静态方法;2、私有方法(因为它是无法被重写的);3、构造方法;、4、父类方法
  • 动态链接:还有一部分符号引用将在每一次运行期间转化为直接引用,这部分称为动态链接,这体现了Java的多态性

2.2、方法返回地址

  • 就是当方法执行完成后,需要返回到调用方的地址,以保证程序的继续往下运行。

2.3、操作数栈

  • 在方法的执行过程中,各种字节码指令会往操作数栈中写入和提取内容,也就是出栈/入栈操作,操作数栈的深度都不会超过在code属性中的maxstacks数据项中设定的最大值,如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

2.4、局部变量表

  • 局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,一个Slot可以存放一个32位以内的数据类型,每个槽都应该能存放一个boolean、byte、char、short,int,float、reference或returnAddress类型的数据。
  • 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的引Slot空间。Java语言中明确的(reference类型则可能是32位也可能是64位),64位的数据类型只有long和double两种。
  • 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。

三、方法调用的字节码指令

  • invokeinterface:抵用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法
  • invokestatic:调用静态方法
  • invokespecial:调用自己的私有方法、构造方法(<init>)以及父类的方法。
  • invokevirtual:调用虚方法,运行期动态查找的过程
  • invokedynamic:动态调用方法

能被invokestatic、invokespecial调用的方法都是静态解析方法(非虚方法),都是能够在解析阶段就能确定的,在类加载阶段就能直接把符号引用转换成直接引用,符合静态解析的方法一共有四种:1、静态方法;2、私有方法(因为它是无法被重写的);3、构造方法;、4、父类的方法

3.1、invokestatic案例

源代码:

package com.brycen.byteclass;

public class MyTest4 {
    public static void test(){
        System.out.println("test invoked");
    }
    public static void main(String[] args) {
        test();
    }
}

字节码:

从字节码中我们可以看到,它使用了invokestatic调用了test方法

...
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #5                  // Method test:()V
         3: return
      LineNumberTable:
        line 15: 0
        line 16: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
...

3.2、方法的静态分派

Grandpa g1 = new Father();

以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father。

  • 静态类型:在运行期间是不会改变的
  • 实际类型:可以在运行期间发生变化,是在运行期方可确定,实则是多态的一种体现。

源代码:

package com.brycen.byteclass;

public class MyTest5 {

    public void test(Grandpa grandpa){
        System.out.println("grandpa");
    }
    public void test(Father father){
        System.out.println("father");
    }
    public void test(Son son){
        System.out.println("son");
    }
     public static void main(String[] args) {
        Grandpa g1 = new Father();
        Grandpa g2 = new Son();

        MyTest5 myTest5 = new MyTest5();
        myTest5.test(g1);
        myTest5.test(g2);
    }
}

class Grandpa{
}
class Father extends Grandpa{
}
class Son extends Father{
}

运行结果:

为什么会出现两个grandpa呢?
实际上在调用test的时候是发生了方法的重载

  • 方法重载是一种静态的行为,而它判断的依据是参数的静态类型,在编译器即可完全确定(从下方的字节码中,我们就可以得知)。
  • 方法重写则是一种动态的行为,到底调用的是哪个对象的方法是在运行期间才能确定的
grandpa
grandpa

部分字节码:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #7                  // class com/brycen/byteclass/Father
         3: dup
         4: invokespecial #8                  // Method com/brycen/byteclass/Father."<init>":()V
         7: astore_1
         8: new           #9                  // class com/brycen/byteclass/Son
        11: dup
        12: invokespecial #10                 // Method com/brycen/byteclass/Son."<init>":()V
        15: astore_2
        16: new           #11                 // class com/brycen/byteclass/MyTest5
        19: dup
        20: invokespecial #12                 // Method "<init>":()V
        23: astore_3
        24: aload_3
        25: aload_1
        26: invokevirtual #13                 // Method test:(Lcom/brycen/byteclass/Grandpa;)V
        29: aload_3
        30: aload_2
        31: invokevirtual #13                 // Method test:(Lcom/brycen/byteclass/Grandpa;)V
        34: return
      LineNumberTable:
        line 22: 0
        line 23: 8
        line 25: 16
        line 26: 24
        line 27: 29
        line 28: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      35     0  args   [Ljava/lang/String;
            8      27     1    g1   Lcom/brycen/byteclass/Grandpa;
           16      19     2    g2   Lcom/brycen/byteclass/Grandpa;
           24      11     3 myTest5   Lcom/brycen/byteclass/MyTest5;
    MethodParameters:
      Name                           Flags
      args

3.3、方法的动态分派

上面说到方法重写是一种动态的行为,到底调用的是哪个对象的方法是在运行期间才能确定的。因此我们可以看到在字节码中的invokevirtual字节码指令后面的字符引用虽然是Fruit(静态类型),但其在运行期间会去查找这个方法的实际类型,这个查找的流程就与invokevirtual的多态查找流程有关的。

invokevirtual的查找流程:

  1. 获取操作数栈顶的那个元素所指向的对象的实际类型
  2. 如果在寻找过程中找到了于字节码常量中的描述符和名称一致的方法,并且具有访问权限的话则返回目标方法的直接引用,则到这里也就结束了
  3. 如果没有找到,则从子类往父类查找。

源代码:

package com.brycen.byteclass;

public class MyTest6 {

    public static void main(String[] args) {
        Fruit apple = new Apple();
        Fruit orange = new Orange();

        apple.test();
        orange.test();

        apple = new Orange();
        apple.test();
    }
}
class Fruit{
    public void test(){
        System.out.println("Fruit");
    }
}
class Apple extends Fruit{
    public void test(){
        System.out.println("Apple");
    }
}
class Orange extends Fruit{
    public void test(){
        System.out.println("Orange");
    }
}

运行结果:

Apple
Orange
Orange

字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/brycen/byteclass/Apple
         3: dup
         4: invokespecial #3                  // Method com/brycen/byteclass/Apple."<init>":()V
         7: astore_1
         8: new           #4                  // class com/brycen/byteclass/Orange
        11: dup
        12: invokespecial #5                  // Method com/brycen/byteclass/Orange."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method com/brycen/byteclass/Fruit.test:()V
        20: aload_2
        21: invokevirtual #6                  // Method com/brycen/byteclass/Fruit.test:()V
        24: new           #4                  // class com/brycen/byteclass/Orange
        27: dup
        28: invokespecial #5                  // Method com/brycen/byteclass/Orange."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method com/brycen/byteclass/Fruit.test:()V
        36: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 16: 16
        line 17: 20
        line 19: 24
        line 20: 32
        line 21: 36
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      37     0  args   [Ljava/lang/String;
            8      29     1 apple   Lcom/brycen/byteclass/Fruit;
           16      21     2 orange   Lcom/brycen/byteclass/Fruit;
    MethodParameters:
      Name                           Flags
      args

四、基于栈指令集和寄存器指令集

4.1、JVM的解释执行与编译执行

现代JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

  • 解释执行:通过解释器来读取字节码,遇到相应的指令就会去执行该指令。
  • 编译执行:通过即时编辑器(Just In Time,JIT)将字节码转换为本地机器码来执行;现代JVM会根据代码热点来生成相应的本地机器码。

4.2、基于栈指令集和寄存器指令集之间的关系

JVM执行指令时所采取的方式是栈指令集

  • 基于栈的指令集主要的操作由入栈与出栈两种。
  • 基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件紧密关联的,无法做到可移植。
  • 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来完成的,它是在高速缓冲区中进行执行的,速度要快很多。虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。

4.3、栈指令集执行分析案例

源码:

public class MyTest8 {

    public int myCalculate(){
        int a = 10;
        int b = 20;
        int c = 30;
        int d = 40;

        int result = (a+b-c) * d;

        return result;
    }
}

字节码:

...
...
public int myCalculate();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=1
         0: bipush        10	//将10压入操作数栈栈顶
         2: istore_1			//将操作数栈栈顶的值取出后存入局部变量角标为1的变量中
         3: bipush        20	//将20压入操作数栈栈顶
         5: istore_2			//将操作数栈栈顶的值取出后存入局部变量角标为2的变量中
         6: bipush        30	//将30压入操作数栈栈顶
         8: istore_3			//将操作数栈栈顶的值取出后存入局部变量角标为3的变量中
         9: bipush        40	//将40压入操作数栈栈顶
        11: istore        4		//将操作数栈栈顶的值取出后存入局部变量角标为4的变量中
        13: iload_1				//获取局部变量角标为1的值,并压入操作数栈栈顶
        14: iload_2				//获取局部变量角标为2的值,并压入操作数栈栈顶
        15: iadd				//将操作数栈栈顶的两个元素弹出,然后相加后将结果压入栈顶
        16: iload_3				//获取局部变量角标为3的值,并压入操作数栈栈顶
        17: isub				//将操作数栈栈顶的两个元素弹出,然后相减后将结果压入栈顶
        18: iload         4		//获取局部变量角标为4的值,并压入操作数栈栈顶
        20: imul				//将操作数栈栈顶的两个元素弹出,然后相乘后将结果压入栈顶
        21: istore        5		//将操作数栈栈顶的值取出后存入局部变量角标为5的变量中
        23: iload         5		//获取局部变量角标为5的值,并压入操作数栈栈顶
        25: ireturn				//返回栈顶元素,如果该方法的操作数栈栈顶下面还有其他元素则全部丢弃,只将该方法的操作数栈的栈顶元素返回,并压入调用者的方法的操作数栈的栈顶。
...
...
  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值