精华内容
下载资源
问答
  • 1. Java虚拟机栈(Java栈)?...栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息。每一个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。虚拟机栈示意图如下...

    1. Java虚拟机栈(Java栈)

    🌳 虚拟机栈也称为Java栈,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)。

    Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。

    栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息。

    每一个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。

    虚拟机栈示意图如下所示:

    4b5696d9a30882e81f3ff3a9d740a95d.png

    2. 虚拟机栈大小的调整

    Java虚拟机规范允许虚拟机栈的大小固定不变或者动态扩展。

    固定情况下:如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,则抛出StackOverflowError异常;

    可动态扩展情况下:尝试扩展的时候无法申请到足够的内存;或者在创建新的线程的时候没有足够的内存去创建对应的虚拟机栈,则会抛出OutOfMemoryError异常。

    可以通过 java -Xss 设置 Java 线程堆栈大小,或者在idea中 help -> edit vm option中改变大小

    3. 运行时栈帧结构

    每个栈帧包含5个组成部分:局部变量表、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地址(Return Address)和一些附加信息

    b90753be2a55dab269a14b55241cec41.png

    3. 1 局部变量表

    局部变量表(Local Variables)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需的分配的局部变量表的最大容量。

    🌳局部变量表的容量

    局部变量表中32位以内的数据类型(除long和double外)只占用一个slot,64位类型(long和double)占用两个slot。举个例子:

    public class Test {

    public static void hello(String name) {

    Date date = new Date();

    long number = 200L;

    double salary = 6000.0;

    int count = 1;

    }

    }

    查看字节码文件有👇 (还可以在idea中使用jclasslib插件来查看局部变量表信息,使用:build project -> view -> show binaryCode with jclasslib)

    21391a91064d5b2a2decd9112d7d8dc4.png

    根据上图可以看出:String和Date引用类型(reference)分别占用一个slot(第0个和第1个)、long类型的变量占用第2个和第3个slot、double类型的变量占用第4个和第5个slot、而int类型的变量则占用第6个slot。

    🍁 如果执行没有被static修饰的方法,那么局部变量表中第0位索引的变量槽,默认是用于传递方法所属对象实例的引用,也就是this(当前实例对象的引用)

    public class Test {

    public void halo(String name) {

    Date date = new Date();

    int count = 1;

    }

    }

    bce4f87b07aaf2f08c8c3a77fd8bd794.png

    可以看到,非静态方法的局部变量表首位就存放了this对象,这也是静态方法内无法使用this的原因(因为静态方法的局部变量表中没有this对象)。

    🌳 局部变量表容量的大小

    在编译器就可以唯一确定下来,并保存在方法的Code属性的maximum locacl variables数据项中,就拿上面Test类的hello方法来说,其字节码里已经指明了局部变量表的大小:

    public static void hello(java.lang.String);

    descriptor: (Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

    stack=2, locals=7, args_size=1 // locals = 7 就是局部变量表的大小

    0: new #2 // class java/util/Date

    3: dup

    4: invokespecial #3 // Method java/util/Date."":()V

    7: astore_1

    8: ldc2_w #4 // long 200l

    11: lstore_2

    12: ldc2_w #6 // double 6000.0d

    15: dstore 4

    17: iconst_1

    18: istore 6

    20: return

    使用jclasslib插件查看局部变量表信息

    023ea12cdde95e8e57fb3be807345c6c.png

    ⚠️方法的调用

    ✅方法调用并不等同于方法中的代码被执行了。方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪个方法),暂时未涉及方法内部的执行逻辑(具体运行过程)。

    所有方法调用的目标方法都是一个常量池中的符号引用,而不是方法在实际运行时内存布局中的入口地址(即非直接引用)。

    在类加载的解析阶段,会将其中一部分符号引用转化为直接饮用,能够解析的前提就是方法在程序真正运行前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期间是不可改变的。

    也就是说:调用目标在程序代码写好、编译器进行编译那一刻就确定下来了。这类方法的调用被称为解析

    🌳 查看局部变量的作用范围

    public class Test {

    public static void hello(String s){

    int count = 1;

    }

    }

    public static void hello(java.lang.String);

    descriptor: (Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

    stack=1, locals=2, args_size=1

    0: iconst_1

    1: istore_1

    2: return

    LineNumberTable:

    line 10: 0

    line 11: 2

    参考局表变量表信息:根据字节码PC计数器知,s 的起始指令地址为0,则 s 在方法开始时就生效,查看LineNumberTable -> line 10: 0可知,对应的代码在第10行,s 的作用域长度为 3 ,即字节码指令 0 - 2 范围内有效,也就是整个hello方法内都有效。

    0ff96efc0def97f8fa9520635b8660c3.png

    🌳变量槽的重复使用

    当方法体中定义的变量,其作用域并不一定会覆盖整个整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的变量槽就可以交给其他变量来重用。

    3. 2 操作数栈

    🍀 操作数栈(Operand Stack)也常被成为操作栈,是一个后入先出栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。其最大深度在编译时就被写到了Code属性的max_stacks中。

    public class Test {

    public int add() {

    int a = 1;

    int b = 1;

    int c = a + b;

    return c;

    }

    }

    查看.class文件

    329d8c628bd93b6d71ddddca90718a64.png

    🍀 栈中的任何一个元素都可以是任意的Java数据类型,32bit的类型占用一个栈深度,64bit的类型占用两个栈单位深度:

    // 64bit数据类型有 Long和Double

    // 32bit数据类型是除了 Long和Double

    public class Test {

    public void test() {

    Long a = 1L;

    }

    }

    查看.class文件

    8c1108a170392bcd1228ce321c051293.png

    🍀操组数栈在方法运行时的具体执行过程

    操作数栈在方法的执行过程中,根据字节码指令往栈中写入数据或提取数据,即入栈和出栈操作。虽然栈是用数组实现的,但根据栈的特性,对栈中数据访问不能通过索引,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

    下面通过一个例子来感受PC寄存器,局部变量表和操作数栈是如何相互配合完成一次方法的执行,代码如下所示:

    public class Test {

    public void add() {

    int a = 15;

    int b = 1;

    int c = a + b;

    }

    }

    在查看字节码指令之前,先记录下几个入栈出栈的字节码指令含义:

    当int取值 -1 ~ 5 采用iconst指令入栈;

    取值 -128 ~ 127(byte有效范围)采用bipush指令入栈;

    取值 -32768 ~ 32767(short有效范围)采用sipush指令入栈;

    取值 -2147483648 ~ 2147483647(int有效范围)采用ldc指令入栈;

    istore,栈顶元素出栈,保存到局部变量表中;

    iload,从局部变量表中加载数据入栈。

    25f95a462a2cd3692165630c3980ed4f.png

    指令执行过程中,PC寄存器,局部变量表和操作数栈状态如下图所示👇

    40eb06e76f26ff6658e6af84774819ae.png

    19989a4945f1e5fa9447416e745d35c7.png

    8c76579ad790bd3de03067c88240d1c4.png

    b7c012671b553a96e2f0cf21ac5cd918.png

    如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中。

    3. 3 动态链接

    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。Class 文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

    3. 4 方法返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法。

    一种是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层方法的调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

    另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给上层调用者产生任何返回值的。

    无论采用何种退出方式,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的 PC 计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

    方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上次方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令等。

    3.5 附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,成为栈帧信息。

    展开全文
  • JVM系列之java虚拟机栈tip:上面讲了JVM运行时数据区域的程序计数器(PC),这篇文章带大家走进JVM的运行时数据区域JAVA虚拟机栈啥是java虚拟机栈java虚拟机栈和程序计数器一样也是线程私有的,生命周期和线程相同;...

    JVM系列之java虚拟机栈

    tip:上面讲了JVM运行时数据区域的程序计数器(PC)

    ,这篇文章带大家走进JVM的运行时数据区域JAVA虚拟机栈

    59910b7c7eb39f8e71cf0b4437a8d5e6.png

    啥是java虚拟机栈

    java虚拟机栈和程序计数器一样也是线程私有的,生命周期和线程相同;它是Java方法执行的线程内存模型。当一个方法被执行的时候,java会同步创建一个栈帧,这里的栈帧就是栈的元素;每一个栈帧包含局部变量表,操作数栈,动态连接,方法返回地址等信息,一个方法从开始到执行结束对应着虚拟机栈中一个栈帧的入栈和出栈。

    下面放一张图让大家直观的理解一下

    4f209e638caed05b85aaecc3e56aa33e.png

    其中位于栈顶的栈帧是当前栈帧,所对应的方法是当前方法,java的执行引擎所执行的字节码指令值只针对当前栈帧操作。其实这也很好理解,我们通常都是在一个方法内又调用另一个方法,形成一个调用链,这样位于最底层的栈帧就是这个调用链的源头。

    下面为大家一一解释栈帧中的内容

    局部变量表

    局部变量表,顾名思义就是存放局部变量的一个表。它存放的是java编译器生成的各种java的基本数据类型(boolean,byte,char,short,float,long,double),对象的引用,retuenAddess(指向了一条字节码指令的地址)。具体内容就是方法传入的参数(包括实例方法中的this),try-catch中定义的异常,以及方法体中定义的变量。

    局部变量表是以槽(shot)为单位的,其中64位长度(long,double)类型数据占用俩个变量槽,而32位的占一个变量槽。

    看一下上篇文章中我们反编译java代码的字节码文件

    源代码public class Main {

    public static void main(String[] args){

    int a=1;

    int b=2;

    System.out.println(a+b);

    }

    }

    反编译字节码public static void main(java.lang.String[]) throws java.io.IOException;

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

    stack=3, locals=3, args_size=1   //local就是局部变量表的大小

    0: iconst_1

    1: istore_1    //栈顶元素弹出存入变量表的槽1

    2: iconst_2

    3: istore_2    //栈顶元素弹出存入变量表的槽2

    4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

    7: iload_1

    8: iload_2

    9: iadd

    10: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V

    13: return

    LineNumberTable:

    line 18: 0

    line 19: 2

    line 20: 4

    line 21: 13

    LocalVariableTable:

    Start  Length  Slot  Name   Signature

    0      14     0  args   [Ljava/lang/String;

    2      12     1     a   I

    4      10     2     b   I

    Exceptions:

    throws java.io.IOException

    从上面的字节码文件中我们可以看出,在java源代码被编译成class文件后每一个方法的变量表的大小就已经确定(locals的值)。而且JVM是通过索引来操作变量表的,当使用的是32位数据类型时就索引N代表使用第N个变量槽。64位则代表第N和第N+1个变量槽那么JVM是如何来确定局部变量表的大小呢?

    大家先猜一下下面这个方法,JVM会为它分配多大的局部变量表呢?@Test

    public void showLocals(){

    {   //代码块1

    int a=100;

    System.out.println(a);

    }

    {   //2

    int b=200;

    System.out.println(b);

    }

    {  //3

    int c=300;

    System.out.println(c);

    }

    }

    答案:stack=2, locals=2, args_size=1

    上面方法使用了a,b,c三个局部变量,上述代码JVM分配了2个变量槽,可见并不是定义了多少个局部变量就分配相应多大的空间。因为局部变量表和下面要说的操作数栈他们的大小直接决定栈帧的大小,不必要的操作数栈的深度和局部变量表的会浪费内存。所以为了节约内存,java采用的使用复用的思想,当代码执行超出一个局部变量的作用域的时候,这个变量占用的槽就可以被其他的变量重用,javac编译器会根据同时生存的最大的局部变量数量和类型计算出locals的大小。

    在初学java的时候,老师都会告诉我们在实例方法中可以通过this代表了调用该方法的对象的引用,而这个this就是在javac编译的时候自动给传入方法的,它被放置在变量槽0的位置,所以在上述代码块中,a,b,c共用一个变量槽,而this使用一个变量槽。对象引用

    后面我会提到堆是java大多数对象分配内存的内存区域,而对象引用不一定就是对象在堆的内存地址还有可能是一个指向对象的句柄。这取决于JVM对对象访问方式的实现。

    操作数栈

    Operand Stack,可以理解为存放操作数的栈。它的大小也是在编译期就已经确定号了的,就是上面反编译代码中出现的stack,栈元素可以是包括long和double在内的任意的java数据类型。

    当一个方法刚开始执行的时候,操作数栈是空的,在方法执行的过程中字节码指令会往操作数栈内写入和取出元素。

    看一下代码public static void main(java.lang.String[]) throws java.io.IOException;

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

    stack=3, locals=3, args_size=1  //栈深度最大为3,3个变量槽

    0: iconst_1             //常量1压入栈

    1: istore_1             //栈顶元素出栈存入变量槽1

    2: iconst_2             //常量2压入栈

    3: istore_2             //栈顶元素出栈存入变量槽2

    4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

    //调用静态方法main

    7: iload_1           //将变量槽1中值压入栈

    8: iload_2           //将变量槽2中值压入栈

    9: iadd              //从栈顶弹出俩个元素相加并且压入栈

    10: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V

    //调用虚方法

    13: return   //返回

    可以看出,在方法的执行过程中会有各种的字节码指令往操作数栈中写入和读出元素。而且操作数栈中的元素的数据类型必须和字节码指令操作的数据的数据类型相匹配,例如istore_2对int类型操作,而如果此时的栈顶元素是long占用俩个变量槽,那么后面的指令操作肯定都会出错。在类加载的时候,检验阶段会进行验证。

    不知道大家听没听说过java的指令集架构是基于栈的,其实从这个就可以佐证这句话,而C语言则是基于寄存器的指令集架构,它底层参数的传递,操作变量以及对内存的访问都是通过读取寄存器中的值实现的。JVM对操作数栈的优化

    在概念模型中,俩个方法的栈帧相互之间是完全独立的。但是很多JVM都对栈帧进行了优化处理,使得俩个栈帧出现一部分重叠。让下面栈帧的部分操作数数栈与上面栈帧的部分局部变量重叠在一起,这样就节约了一些空间,而且进行方法调用的时候不用进行额外的参数传递和可以共用一部分数据

    例如下面的代码public class Main {

    public int getA(){

    int a=1;

    a++;

    return a;

    }

    public static void main(String[] args) {

    Main m=new Main();

    int a=m.getA();

    System.out.println(a);

    }

    }

    按照概念模型的设计,getA方法最后会执行ireturn指令,从栈顶弹出int类型元素然后返回,在main方法调用getA处会将返回值压入栈后再存入变量槽。

    但是优化后,main方法对于的栈帧在getA方法的下面,因为main方法的操作数栈和getA方法对应栈帧的局部变量表部分重合,所以就不用返回a,而是直接放入变量槽中,在main方法中弹出即可。

    f47324ea1faa2e02fa156ce35e3fb039.png

    动态连接关于动态连接的内容,可以在阅读完后面运行时常量池,方法调用相关内容后再做理解

    每一个栈帧中都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个是为了方法调用过程中的动态连接。在每一个class文件中都会包含一个常量池,这个常量池中有大量的符号引用(通过符号无歧义的指向一个目标),这些符号引用一部分会在类加载阶段转换为直接引用(直接指向目标的指针,相对偏移量或者是可以定位到目标的句柄)即静态解析,另一部分在运行期转换为直接引用即动态连接。

    方法返回地址

    在方法调用结束后,必须返回到该方法最初被调用时的位置,程序才能继续运行,所以在栈帧中要保存一些信息,用来帮助恢复它的上层主调方法的执行状态。方法返回地址就可以是主调方法在调用该方法的指令的下一条指令的地址。

    重磅资源!!!

    关注小白不想当码农微信公众号。

    后台回复java核心技术卷关键字领取《java核心技术卷》pdf

    回复jvm领取《深入理解Java虚拟机》pdf和《自己动手写jvm》

    回复设计模式领取《headfirst设计模式》pdf

    回复计算机网络领取《计算机网络自顶向下》pdf

    最后

    我是不想当码农的小白,平时会写写一些技术博客,推荐优秀的程序员博主给大家还有自己遇到的优秀的java学习资源,希望和大家一起进步,共同成长。

    以上内容如有错误,还望指出,感谢

    公众号点击交流,添加我的微信,一起交流编程呗!

    公众号: 小白不想当码农

    展开全文
  • 虚拟机栈

    2020-07-06 21:10:28
    每个方法发执行都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、方法返回地址、动态链接、一些附加信息每个方法的调用直至执行完过程,对应着一个栈帧在虚拟机栈中找到出栈的过程 局部变量表 局部...

    Java虚拟机栈

    虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型;每个方法发执行都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、方法返回地址、动态链接、一些附加信息每个方法的调用直至执行完过程,对应着一个栈帧在虚拟机栈中找到出栈的过程
    在这里插入图片描述

    局部变量表

    局部变量表存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)和returnAddress类型局部变量表所需要的内存空间在编译期完全分配,方法在运行期间v不会改变就局部变量表的大小。

    操作数栈

    虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

    虚拟机栈的两种异常

    一、如果线程的请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(栈溢出异常)
    二、如果虚拟机栈可以动扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常(内存不足)

    一起学习交流的可以加下面的QQ群:

    515131162

    点击链接加入群聊【Java从入门到放弃】:https://jq.qq.com/?_wv=1027&k=pYNPNi6q

    展开全文
  • 线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。每个方法从调用到执行完成的过程,就对应着一个...

    特点

    Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。

    Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧。线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程。

    Java 虚拟机栈使用的内存不需要保证是连续的。

    Java 虚拟机规范即允许 Java 虚拟机栈被实现成固定大小(-Xss),也允许通过计算结果动态来扩容和收缩大小。如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候就已经确定。

    Java 虚拟机栈会出现的异常

    如果线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出 StackOverflowError 异常。

    如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。

    Java 虚拟机栈执行过程

    栈帧(Stack Frame)

    栈帧存在于 Java 虚拟机栈中,是 Java 虚拟机栈中的单位元素,每个线程中调用同一个方法或者不同的方法,都会创建不同的栈帧(可以简单理解为,一个线程调用一个方法创建一个栈帧),所以,调用的方法链越多,创建的栈帧越多(代表作:递归)。在 Running 的线程,只有当前栈帧有效(Java 虚拟机栈中栈顶的栈帧),与当前栈帧相关联的方法称为当前方法。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。

    栈帧结构如图:

    ecfcc9fb1de7

    栈帧结构

    局部变量表(Local Variable Table)

    每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译成 Class 文件时,在 Class 文件格式属性表中 Code 属性的 max_locals(局部变量表所需的存储空间,单位是 Slot) 数据项中确定了需要分配的局部变量表的最大容量。

    局部变量表的容量以变量槽(Variable Slot)为最小单位,不过 Java 虚拟机规范中并没有明确规定每个 Slot 所占据的内存空间大小,只是有导向性地说明每个 Slot 都应该存放的8种类型: byte、short、int、float、char、boolean、reference(对象引用就是存到这个栈帧中的局部变量表里的,这里的引用指的是局部变量的对象引用,而不是成员变量的引用。成员变量的对象引用是存储在 Java 堆(Heap)中)、returnAddress(虚拟机数据类型,Sun JDK 1.4.2版本之前使用 jsr/ret 指令用于进行异常处理,后续版本已废弃这种实现方式,目前使用异常处理器表代替)类型的数据,这8种类型的数据,都可以使用32位或者更小的空间去存储。Java 虚拟机规范允许 Slot 的长度可以随着处理器、操作系统或者虚拟机的不同而发生变化。对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。即 long 和 double 两种类型。做法是将 long 和 double 类型速写分割为32位读写的做法。不过由于局部变量表建立在线程的堆栈上,是线程的私有数据,无论读写两个连续的 Slot 是否是原子操作,都不会引起数据安全问题。

    Java 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的 Slot 数量。如果是32位数据类型的数据,索引 n 就表示使用第 n 个 Slot,如果是64位数据类型的变量,则说明要使用第 n 和第 n+1 两个 Slot。

    在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法(非 static 方法),那么局部变量表中的第0位索引的 Slot 默认是用来传递方法所属对象实例的引用,在方法中可以通过关键字 this 来访问这个隐含的参数。其余参数按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。

    局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间,但也有可能会影响到系统的垃圾收集行为。

    局部变量无初始值(实例变量和类变量都会被赋予初始值),类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予开发者定义的值。因此即使在初始化阶段开发者没有为类变量赋值也没有关系,类变量仍然具有一个确定的默认值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。

    使用一段代码说明一下局部变量表:

    // java 代码

    public int test() {

    int x = 0;

    int y = 1;

    return x + y;

    }

    // javac 编译后的字节码,使用 javap -v 查看

    public int test();

    descriptor: ()I

    flags: ACC_PUBLIC

    Code:

    stack=2, locals=3, args_size=1

    0: iconst_0

    1: istore_1

    2: iconst_1

    3: istore_2

    4: iload_1

    5: iload_2

    6: iadd

    7: ireturn

    LineNumberTable:

    line 7: 0

    line 8: 2

    line 9: 4

    LocalVariableTable:

    Start Length Slot Name Signature

    0 8 0 this Lcom/alibaba/uc/TestClass;

    2 6 1 x I

    4 4 2 y I

    对应上面的解释说明,通过 LocalVariableTable 也可以看出来:

    Code 属性:

    stack(int x(1个栈深度)+ int y(1个栈深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隐含参数)=1

    验证 Slot 复用,运行以下代码时,在 VM 参数中添加 -verbose:gc:

    public void test() {

    {

    byte[] placeholder = new byte[64 * 1024 * 1024];

    }

    int a = 0; // 当这段代码注释掉时,System.gc() 执行后,也并不会回收这64MB内存。当这段代码执行时,内存被回收了

    System.gc();

    }

    局部变量表中的 Slot 是否还存在关于 placeholder 数组对象的引用。当 int a = 0; 不执行时,代码虽然已经离开了 placeholder 的作用域,但是后续并没有任何对局部变量表的读写操作,placeholder 原本所占用的 Slot 还没有被其他变量所复用,所以 placeholder 作为 GC Roots(所有 Java 线程当前活跃的栈帧里指向 Java 堆里的对象的引用) 仍然是可达对象。当 int a = 0; 执行时,placeholder 的 Slot 被变量 a 复用,所以 GC 触发时,placeholder 变成了不可达对象,即可被 GC 回收。

    操作数栈(Operand Stack)

    操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。

    同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。

    操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。

    当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd(使用 iadd 指令时,相加的两个元素也必须是 int 型) 在运行的时候将操作数栈中最接近栈顶的两个 int 数值元素出栈相加,然后将相加结果入栈。

    以下代码会以什么形式进入操作数栈?

    // java 代码

    public void test() {

    byte a = 1;

    short b = 1;

    int c = 1;

    long d = 1L;

    float e = 1F;

    double f = 1D;

    char g = 'a';

    boolean h = true;

    }

    // 字节码指令

    0: iconst_1 // 把 a 压入操作数栈栈顶

    1: istore_1 // 将栈顶的 a 存入局部变量表索引为1的 Slot

    2: iconst_1 // 把 b 压入操作数栈栈顶

    3: istore_2 // 将栈顶的 b 存入局部变量表索引为2的 Slot

    4: iconst_1 // 把 c 压入操作数栈栈顶

    5: istore_3 // 将栈顶的 c 存入局部变量表索引为3的 Slot

    6: lconst_1 // 把 d 压入操作数栈栈顶

    7: lstore 4 // 将栈顶的 d 存入局部变量表索引为4的 Slot,由于 long 是64位,所以占2个 Slot

    9: fconst_1 // 把 e 压入操作数栈栈顶

    10: fstore 6 // 将栈顶的 e 存入局部变量表索引为6的 Slot

    12: dconst_1 // 把 f 压入操作数栈栈顶

    13: dstore 7 // 将栈顶的 f 存入局部变量表索引为4的 Slot,由于 double 是64位,所以占2个 Slot

    15: bipush 97 // 把 g 压入操作数栈栈顶

    17: istore 9 // 将栈顶的 g 存入局部变量表索引为9的 Slot

    19: iconst_1 // 把 h 压入操作数栈栈顶

    20: istore 10 // 将栈顶的 h 存入局部变量表索引为10的 Slot

    从上面字节码指令可以看出来,除了 long、double、float 类型使用的字节码指令不是 iconst 和 istore,其他类型都是使用这两个字节码指令操作,说明 byte、short、char、boolean 进入操作数栈时,都会被转化成 int 型。

    在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机实现会做一些优化,令两个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无需进行额外的参数复制传递。

    ecfcc9fb1de7

    栈帧共享

    Java 虚拟机的解释执行引擎称为 “基于栈的执行引擎”,其中所指的 “栈” 就是操作数栈。

    动态连接(Dynamic Linking)

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

    在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1.类的全限定名,2.字段名和属性,3.方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

    看看以下代码的 Class 文件格式的常量池:

    // java 代码

    public Test test() {

    return new Test();

    }

    // 字节码指令

    Constant pool:

    #1 = Methodref #4.#19 // java/lang/Object."":()V

    #2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I

    #3 = Class #21 // com/alibaba/uc/Test

    #4 = Class #22 // java/lang/Object

    #5 = Utf8 i

    #6 = Utf8 I

    #7 = Utf8

    #8 = Utf8 ()V

    #9 = Utf8 Code

    #10 = Utf8 LineNumberTable

    #11 = Utf8 LocalVariableTable

    #12 = Utf8 this

    #13 = Utf8 Lcom/alibaba/uc/Test;

    #14 = Utf8 test

    #15 = Utf8 ()I

    #16 = Utf8

    #17 = Utf8 SourceFile

    #18 = Utf8 Test.java

    #19 = NameAndType #7:#8 // "":()V

    #20 = NameAndType #5:#6 // i:I

    #21 = Utf8 com/alibaba/uc/Test

    #22 = Utf8 java/lang/Object

    public int test();

    descriptor: ()I

    flags: ACC_PUBLIC

    Code:

    stack=1, locals=1, args_size=1

    0: getstatic #2 // Field i:I

    3: areturn

    LineNumberTable:

    line 8: 0

    LocalVariableTable:

    Start Length Slot Name Signature

    0 4 0 this Lcom/alibaba/uc/Test;

    从上面字节码指令看出 0: getstatic #2 // Field i:I 这行字节码指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 为符号引用,在类加载过程的解析阶段会被转化为直接引用(指向方法区的指针)。

    方法返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:areturn),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

    无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

    方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。

    简述:

    虚拟机会使用针对每种返回类型的操作来返回,返回值将从操作数栈出栈并且入栈到调用方法的方法栈帧中,当前栈帧出栈,被调用方法的栈帧变成当前栈帧,程序计数器将重置为调用这个方法的指令的下一条指令。

    附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

    展开全文
  • java虚拟机之虚拟机栈

    2020-10-25 09:32:59
    文章目录一、虚拟机栈描述栈的存储单位栈帧栈帧内容局部变量表slot操作数栈(Operand Stack)字节码demo栈顶缓存技术动态链接(指向运行时常量池的方法引用)静态链接动态链接虚方法表方法返回地址 一、虚拟机栈 包含...
  • JVM虚拟机栈

    2020-09-12 14:55:47
    栈帧包括局部变量表、操作数栈、动态链接、方法返回地址等信息。 虚拟机栈中会出现两种异常: StackOverflowError:线程请求的栈的深度大于虚拟机所允许的深度。 OutOfMemoryError:如果虚拟机栈的容量是可以动态...
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出的时候,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回地址是要通过异常表来确定的,...
  • 学习JVM的一点简单笔记,宋红康老师jvm讲的很好,感谢。 尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 方法返回地址 附加信息 相关试题
  • Java虚拟机栈详解

    2021-01-23 22:44:38
    栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息 每个方法被调用至执行完毕的过程,就对应这个栈帧在虚拟机栈中从入栈到出栈的完整过程 栈的深度有限制 局部变量表 局部变量表(Local ...
  • 方法返回地址 局部变量表 可以将局部变量表理解为一个数组,数组中的内容为所在的栈帧对应的方法上参数以及方法体内的局部变量,可以有基本数据类型、对象引用(真正的对象在堆上)。 既然是数组,而数组的长度是...
  • 虚拟机栈知识概括

    2020-10-06 20:42:37
    虚拟机栈知识概括虚拟机栈概述栈的存储单位局部变量表操作数栈栈顶缓存技术动态链接解析和分派方法返回地址一些附加信息 虚拟机栈概述 虚拟机栈出现的背景: 由于跨平台性的设计,Java的指令都是根据栈来设计的。...
  • 虚拟机栈大小的调整局部变量表操作数栈动态链接方法返回地址附加信息相关问题1.什么情况下会发生栈内存溢出?2.如果让你写一段栈溢出的代码你会什么写3.一个栈大概有多大?4.每个线程都有这样大小的一个栈吗?5.JVM ...
  • 虚拟机栈—动态链接 动态链接、方法返回地址、附加信息 :有些地方被称为帧数据区; 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,这个引用的目的就是为了支持当前方法的代码能够实现动态...
  • 0032-虚拟机栈

    2020-06-03 16:12:30
    文章目录1 简介2 配置3 栈帧3.1 局部变量表(local variables)3.2 操作数(Operand Stack)3.3 动态链接(Dynamic Linking)3.4 方法返回地址(Return Address)3.5 一些附加信息4. 的相关面试题 1 简介 java...
  • JVM--虚拟机栈

    2019-08-15 11:23:17
    虚拟机栈概念 目录 虚拟机栈概念 栈帧 代码演示 ... 虚拟机栈是线程独占区域,... 栈帧由局部变量表、操作数栈、动态链接、返回地址组成,还有一些其它无关的数据 每一个方法从调用开始至执行完成的过程,都...
  • 操作数 Operand Stack 每一个独立的栈帧除了包含局部变量表之外,还包含一个FIFO的操作数,也成为表达式Expression Stack 操作数,在方法执行过程中,根据字节码指令,往中写入数据或提取数据,即Push Pop 某...
  • 如果在类型C中找到与常量池中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束:如果不通过,则返回IllegalAccessError异常。 否则按照继承关系从下往上一次对...
  • 虚拟机栈3.1概念2. 栈内存溢出3.3 存储结构和原理3.4 内部结构4. 局部变量表4.1 变量槽Slot4.2 静态变量 VS局部变量5. 操作数栈3.4.5 栈顶缓存技术6. 动态链接6.1 概念6.2 方法的调用7. 方法返回地址8. 附加信息9. ...
  • JVM【虚拟机栈

    2019-05-07 11:42:50
    方法返回地址 JVM虚拟机栈简介 JVM(Java Virtual Machine)即Java虚拟机的内存模型按照是否被线程共享可分为两部分: 可被线程共享: 堆 和 方法区 线程私有:虚拟机栈 和 本地方法栈 在JVM...
  • 五-虚拟机栈

    2020-08-27 12:09:45
    5.1 概述 是运行时的单位,而堆是存储的单位。...主管Java程序的运行,保存方法的局部变量(8种基本类型、对象的引用地址)、部分结果,并参与方法的调用和返回 虚拟机中遇到的异常 Java虚拟机允许J
  • 原创 | jvm02、java虚拟机栈

    千次阅读 2018-11-15 20:59:59
    小白带你快速了解! java虚拟机栈是jvm内存结构中的...既然是栈,肯定有个什么玩意入栈和出栈,java虚拟机栈主要是用来存放线程运行方法时所需的数据,指令和方法返回地址等,那么靠什么存储?这就需要栈帧,这么个...
  • JVM 虚拟机栈详解

    千次阅读 2018-11-09 17:37:22
    栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方能从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程 。 在编译程序代码的时候,栈帧中需要多大的...
  • JVM系列之java虚拟机栈

    2020-09-16 21:29:53
    JVM系列之java虚拟机栈 tip:上面讲了JVM运行时...每一个栈帧包含局部变量表,操作数栈,动态连接,方法返回地址等信息,一个方法从开始到执行结束对应着虚拟机栈中一个栈帧的入栈和出栈。 下面放一张图让大家直观的理
  • java 栈帧 虚拟机栈

    2017-04-10 11:22:15
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。...
  • 深入理解Java虚拟机栈的栈帧

    千次阅读 多人点赞 2019-08-25 00:05:48
    本节将会介绍一下Java虚拟机栈中的栈帧,会对栈帧的组成部分(局部变量表、操作数栈、动态链接、方法出口)分别进行介绍,最后...方法的返回地址 结合javap命令理解栈帧 Java虚拟机栈概述 Java虚拟机栈(Java Vir...
  • 【JVM】5、虚拟机栈

    2021-01-17 22:37:03
    虚拟机栈虚拟机栈概述虚拟机栈的基本内容虚拟机栈的主要特点虚拟机栈的异常设置栈内存大小栈的存储单位栈的运行原理栈帧的内部结构局部变量表局部变量表概念字节码中方法内部结构的剖析局部变量表的基本存储单元:...
  • JVM:Java虚拟机栈

    2020-08-31 00:33:22
    Java虚拟机栈JVM栈栈帧(Stack Frame)局部变量表操作数栈动态链接方法返回地址 JVM栈 Java虚拟机栈主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不...
  • JVM第五章_虚拟机栈

    2020-06-18 19:42:55
    Stack-Cashing)技术6、动态链接动态链接(或指向运行时常量池的方法引用)7、方法的调用虚方法与非虚方法关于invokedynamic指令方法重写的本质虚方法表8、方法中的返回地址(Return Address)9、一些附加信息10、的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 731
精华内容 292
关键字:

虚拟机栈返回地址