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会为它调配多大的局部变量表呢?

@Testpublic 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学习资源,心愿和大家一起提高,独特成长。

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

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

公众号: 小白不想当码农