关于java:JVM入门之虚拟机栈

39次阅读

共计 4174 个字符,预计需要花费 11 分钟才能阅读完成。

JVM 系列之 java 虚拟机栈

tip:下面讲了 JVM 运行时数据区域的程序计数器(PC)
,这篇文章带大家走进 JVM 的运行时数据区域 JAVA 虚拟机栈

啥是 java 虚拟机栈

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

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

其中位于栈顶的栈帧是以后栈帧,所对应的办法是以后办法,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 办法中弹出即可。


动静连贯

对于动静连贯的内容,能够在浏览完前面运行时常量池,办法调用相干内容后再做了解

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


办法返回地址

在办法调用完结后,必须返回到该办法最后被调用时的地位,程序能力持续运行,所以在栈帧中要保留一些信息,用来帮忙复原它的下层主调办法的执行状态。办法返回地址就能够是主调办法在调用该办法的指令的下一条指令的地址。


重磅资源!!!

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

后盾回复 java 核心技术卷 关键字支付《java 核心技术卷》pdf

回复 jvm 支付《深刻了解 Java 虚拟机 》pdf 和《 本人入手写 jvm

回复 设计模式 支付《headfirst 设计模式》pdf

回复 计算机网络 支付《计算机网络自顶向下》pdf

最初

我是不想当码农的小白,平时会写写一些技术博客,举荐优良的程序员博主给大家还有本人遇到的优良的 java 学习资源,心愿和大家一起提高,独特成长。

以上内容如有谬误,还望指出,感激

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

公众号: 小白不想当码农

正文完
 0