Java内存结构之虚拟机栈

4 篇文章 0 订阅
2 篇文章 0 订阅

虚拟机栈也被很多人称为Java栈。它是线程私有的,虚拟机栈描述的是Java方法执行的内存结构。
虚拟机栈的内存结构图
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈的数据结构是先进后出。

栈帧: 是用来存储数据和部分过程结果的数据结构。
栈帧的位置:  内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
栈帧大小确定时间: 编译期确定,不受运行期数据影响。

通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展[插图],当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

2. Java虚拟机运行时栈帧结构

2.1 栈帧是什么?

栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元。
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

2.2 栈帧在什么地方?
内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> 这里就是栈帧了
2.3 栈帧的含义?

每个方法的执行和结束对应着栈帧的入栈和出栈。
入栈表示被调用,出栈表示执行完毕或者返回异常。
一个虚拟机栈对应一个线程,当前CPU调度的那个线程叫做活动线程;一个栈帧对应一个方法,活动线程的虚拟机栈里最顶部的栈帧代表了当前正在执行的方法,而这个栈帧也被叫做“当前栈帧”。

2.4 栈帧既然是个数据结构,都有哪些数据?
  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 附加信息
2.5 栈帧的大小是什么时候确定的?

编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。不会受到运行期数据的影响。

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

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

但是,虚拟机进行了一些优化:为了避免过多的 方法间参数的复制传递、方法返回值的复制传递 等一些操作,就让一部分数据进行栈桢间共享。


3. 局部变量表

3.1 什么是局部变量表

是一片逻辑连续的内存空间,最小单位是Slot,用来存放方法参数和方法内部定义的局部变量。我觉得可以想成Slot数组…

JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”

手动翻译:任何参数都从从局部变量0开始的连续局部变量中传递。

虚拟机没有明确指明一个Slot的内存空间大小。但boolean,byte,char,short,int,float,reference,returnAddress类型的数据都可以用32位(bit位)空间或者更小的内存来存放。这些类型占用一个Slot。Java中的long和double类型是64位(bit位),占用两个Slot。(只有double和long是jvms里明确规定的64位数据类型)。

通过反编译证实long是否占用两个Slot

源码

public class Math {

    void compute(){
        int i = 1;
        long l = 2L;
        Object o = new Object();
    }

}

通过运行javap -v Math.class 得编译后的字节码

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=5, args_size=1
         0: iconst_1
         1: istore_1
         2: ldc2_w        #2                  // long 2l
         5: lstore_2
         6: new           #4                  // class java/lang/Object
         9: dup
        10: invokespecial #1                  // Method java/lang/Object."<init>":()V
        13: astore        4
        15: return
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 6
        line 10: 15
      LocalVariableTable://本地(局部)变量表
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lspring/cache/jvm/Math; //可以看出this在Slot的第0位。
            2      14     1     i   I //int i变量在Slot的第1位
            6      10     2     l   J //long l变量在Slot的第2位
           15       1     4     o   Ljava/lang/Object;//Object 由于long整型占据两个Slot位所以o变量在Slot的第4位
}
SourceFile: "Math.java"
3.2 虚拟机如何调用这个局部变量表?

局部变量表是有索引的,就像数组一样。从0开始,到表的最大索引,也就是Slot的数量-1。
要注意的是,方法参数的个数 + 局部变量的个数 ≠ Slot的数量。因为Slot的空间是可以复用的,当pc计数器的值已经超出了某个变量的作用域时,下一个变量不必使用新的Slot空间,可以去覆盖前面那个空间。(部分内容在P183页)

特别地,JVMS7:

On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)

手动翻译:在一个实例方法的调用时,局部变量表的第0位是一个指向当前对象的引用,也就是Java里的this。

通过反编译证实局部变量表的第0位是否是this

源码

public class Math {

    void compute(){
    }

}

通过运行javap -v Math.class 得编译后的字节码

 void compute();
   descriptor: ()V
   flags:
   Code:
     stack=0, locals=1, args_size=1
        0: return
     LineNumberTable:
       line 8: 0
     LocalVariableTable: //本地(局部)变量表
       Start  Length  Slot  Name   Signature
           0       1     0  this   Lspring/cache/jvm/Math;//可以看出this在Slot的第0位。
SourceFile: "Math.java"

4.操作数栈

Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.

翻译:每个栈帧都包含一个被叫做操作数栈的后进先出的栈。叫操作栈,或者操作数栈。

Where it is clear by context, we will sometimes refer to the operand stack of the current frame as simply the operand stack.

翻译:通常情况下,操作数栈指的就是当前栈桢的操作数栈。

4.1 操作数栈有什么用?

操作数栈是对一些数值进行运算,任何在方法内定义的数值或引用都是先进操作数栈后再存进局部变量表,当局部变量表中的某些变量需要参与运算则应该加载进操作数栈进行操作。

通过反编译阅读字节码理解操作数栈的作用

源码

public class Math {

    int compute(){

        int price = 1000;
        price = price + 999;
        return price;

    }

}

通过运行javap -v Math.class 得编译后的字节码

  int compute();
    descriptor: ()I
    flags:
    Code:
      stack=2, locals=2, args_size=1
         0: sipush        1000 //将数值1000推送至栈顶(操作数栈)
         3: istore_1 //将栈顶int型数值存入局部变量表中Slot为1的变量中。(也就是price)
         4: iload_1 //将局部变量表中Slot为1的变量加载进操作数栈。(也就是price)
         5: sipush        999 //将整型数值999推送至栈顶(操作数栈)
         8: iadd //操作数栈中的两个int整型进行相加并重新压入操作数栈
         9: istore_1 //将栈顶int型数值存入局部变量表中Slot为1的变量中。(也就是price)
        10: iload_1 //将局部变量表中Slot为1的变量加载进操作数栈。(也就是price)
        11: ireturn //将操作数栈中的int整型数值返回
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lspring/cache/jvm/Math;
            4       8     1 price   I
}
SourceFile: "Math.java"

通过上面阅读字节码,可以看出在将数值赋值到变量中时,先进操作数栈再存入局部变量表中。在参与运算时再加载进操作数栈。方法返回的也是将操作数栈里的数值进行返回。

The operand stack is empty when the frame that contains it is created. The Java virtual machine supplies instructions to load constants or values from local variables or fields onto the operand stack. Other Java virtual machine instructions take operands from the operand stack, operate on them, and push the result back onto the operand stack. The operand stack is also used to prepare parameters to be passed to methods and to receive method results.

翻译+归纳:

1.栈桢刚创建时,里面的操作数栈是空的。
2.Java虚拟机提供指令来让操作数栈对一些数据进行入栈操作,比如可以把局部变量表里的数据、实例的字段等数据入栈。
3.同时也有指令来支持出栈操作。
4.向其他方法传参的参数,也存在操作数栈中。
5.其他方法返回的结果,返回时存在操作数栈中。

4.2 操作数栈本身就是一个普通的栈吗?

其实栈就是栈,再加上数据结构所支持的一些指令和操作。
但是,这里的栈也是有约束的。
操作数栈是区分类型的,操作数栈中严格区分类型,而且指令和类型也好严格匹配。

小结:
1.操作数栈其实本身就是一个栈数据结构,加上一些对字节码操作的指令,对栈中的数据进行操作。
2.操作数栈中的数据是按照严格的类型区分的,操作不同类型的数据,也需用不同的指令进行操作。 例如iadd是将操作数栈顶两int型数值相加并将结果压入栈顶而ladd是将栈顶两long型数值相加并将结果压入栈顶。


5. 动态链接

5.1 什么是动态链接

一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。

名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?
需要解析成相应的直接引用,利用直接引用来准确地找到。

举个例如,就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个地址起了个别名叫A,以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。

这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

通过反编译阅读字节码理解动态链接

源码

package spring.cache.jvm;

public class Math {

    void compute(){
        test();
    }

    void test(){
        String methodName = "test";
    }
}

通过运行javap -v Math.class 得编译后的字节码

public class spring.cache.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //常量池
// 符号引用 = 引用类型	符号引用.符号引用
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = Methodref          #4.#20         // spring/cache/jvm/Math.test:()V
   #3 = String             #14            // test
   #4 = Class              #21            // spring/cache/jvm/Math
   #5 = Class              #22            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lspring/cache/jvm/Math;
  #13 = Utf8               compute
  #14 = Utf8               test
  #15 = Utf8               methodName
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               SourceFile
  #18 = Utf8               Math.java
  #19 = NameAndType        #6:#7          // "<init>":()V
  #20 = NameAndType        #14:#7         // test:()V
  #21 = Utf8               spring/cache/jvm/Math
  #22 = Utf8               java/lang/Object
{
  public spring.cache.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lspring/cache/jvm/Math;

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0 //加载局部变量表Slot为0的变量 也就是 this
         1: invokevirtual #2                  // Method test:()V  //调用常量池符号引用为#2的方法。
         4: return //无返回值 所以直接返回
      LineNumberTable:
        line 7: 0
        line 10: 4
      LocalVariableTable: //局部变量表
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lspring/cache/jvm/Math;

  void test();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #3                  // String test //将常量池符号引用为#3的常量值中推送至操作数栈中
         2: astore_1 //将操作数栈中的引用类型存入局部变量表Slot位为1的变量中,也就是methodName。(字符串是引用类型)
         3: return //无返回值 所以直接返回
      LineNumberTable:
        line 14: 0
        line 16: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature //局部变量表
            0       4     0  this   Lspring/cache/jvm/Math;
            3       1     1 methodName   Ljava/lang/String;
}
SourceFile: "Math.java"

通过阅读字节码我们将可以看到compute方法调用test的过程。
步骤:
1.invokevirtual #2 通过invokevirtual指令调用常量池符号引用为#2的方法。
2.通过查看常量池发现#2符号引用对应的是 #2 = Methodref #4.#20 // spring/cache/jvm/Math.test:()V。 通过查看#2对应的类型是一个Methodref(方法引用) 后续跟#4.#20
3.通过查看#4的符号引用我们看到#4 = Class #21 // spring/cache/jvm/Math,我们得到Math的类元信息。后续跟着#21.。
4.通过查看#21的符号引用我们看到#21 = Utf8 spring/cache/jvm/Math 是一个UTF-8的字符串,该字符串是Math的全限定名。
5.回到第2步,我们定位到了#4得到了Math对象的信息,那么#4调用的#20是什么呢?
6.通过查看#20的符号引用我们看到#20 = NameAndType #14:#7 // test:()V#20是一个NameAndType后续跟着#14:#17
7.通过查看#14的符号引用我们看到#14 = Utf8 test 是一个UTF-8的字符串该字符串正是我们要调用的方法的方法名。
8.回到第6步再看#14冒号后面的#7是什么呢?
9.通过查看#7的符号引用我们看到#7 = Utf8 ()V是一个UTF-8的字符串该字符串正是我们要调用的方法类型()代表这是一个方法V代表该方法的返回值void
10.回到第6步看#20 = NameAndType #14:#7 // test:()V#14:#7组合起来就是 test:()V
11.回到第2步#4.#20 其实就是 spring/cache/jvm/Math.test:()V。从而invokevirtual #2调用的就是spring/cache/jvm/Math类的test:()V方法。

5.1 动态链接的前提

每一个栈帧内部都有包含一个指向运行时常量池的引用,来支持动态链接的实现。


6. 方法返回地址

6.1 方法正常调用完成

返回一个值给调用它的方法,方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令的时候,使用哪种返回指令取决于方法返回值的数据类型(如果有返回值的话)。

JVMS7中的2.6.4 Normal Method Invocation Completion中写道:

This occurs when the invoked method executes one of the return instructions (§2.11.8), the choice of which must be appropriate for the type of the value being returned (if any).

手动翻译+理解:Java虚拟机根据不同数据类型有不同的底层return指令。当被调用方法执行某条return指令时,会选择相应的return指令来让值返回(如果该方法有返回值的话)。

The current frame (§2.6) is used in this case to restore the state of the invoker, including its local variables and operand stack, with the program counter of the invoker appropriately incremented to skip past the method invocation instruction. Execution then continues normally in the invoking method’s frame with the returned value (if any) pushed onto the operand stack of that frame.

手动翻译:在这种情况,当前栈帧就被用来恢复调用者的状态,都恢复哪些呢? 恢复局部变量表,操作数栈和程序计数器(pc指针),而这个程序技术器要适当地增加,来指向下一条指令(也就是调用函数的下一句)。使调用者方法能够正常地继续执行下去,而且返回值push(推送)到了调用方法的操作数栈中。

通过反编译阅读字节码理解方法返回地址

源码

package spring.cache.jvm;

public class Math {

    void compute(){
        int number = test();
    }

    int test(){
        return 1000;
    }
}

通过运行javap -v Math.class 得编译后的字节码

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=2, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method test:()I	//调用this.test()方法 test()方法返回一个int类型的数值并存入当前操作数栈中
         4: istore_1 //将操作数栈中的数值赋值到Slot为1的int变量中也就是number
         5: return
      LineNumberTable:
        line 6: 0
        line 7: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lspring/cache/jvm/Math;
            5       1     1 number   I

  int test();
    descriptor: ()I
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: sipush        1000 //将一个短整型常量值 1000 推送至栈顶
         3: ireturn	//返回操作数栈中的int整型的数值
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature //局部变量表
            0       4     0  this   Lspring/cache/jvm/Math;
}
SourceFile: "Math.java"
6.2 方法异常调用完成

异常时不会返回值给调用者。

完。

参考博客

https://www.cnblogs.com/noKing/p/8167700.html

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值