【虚拟机栈】虚拟机栈详解

标签: JVM
16人阅读 评论(0) 收藏 举报
分类:

前言

Java 虚拟机的内存模型分为两部分:一部分是线程共享的,包括 Java 堆和方法区;另一部分是线程私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。

JVM 是基于栈的。但是这个“栈” 具体指的是什么?难道就是虚拟机栈?想要回答这个问题我们先要从虚拟机栈的结构谈起。

虚拟机栈

何为虚拟机栈

虚拟机栈的栈元素是栈帧,当有一个方法被调用时,代表这个方法的栈帧入栈;当这个方法返回时,其栈帧出栈。因此,虚拟机栈中栈帧的入栈顺序就是方法调用顺序。什么是栈帧呢?栈帧可以理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另一部分是操作数栈,用来存放操作数。我们知道,Java 程序编译之后就变成了一条条字节码指令,其形式类似汇编,但和汇编有不同之处:汇编指令的操作数存放在数据段和寄存器中,可通过存储器或寄存器寻址找到需要的操作数;而 Java 字节码指令的操作数存放在操作数栈中,当执行某条带 n 个操作数的指令时,就从栈顶取 n 个操作数,然后把指令的计算结果(如果有的话)入栈。因此,当我们说 JVM 执行引擎是基于栈的时候,其中的“栈”指的就是操作数栈。举个简单的例子对比下汇编指令和 Java 字节码指令的执行过程,比如计算 1 + 2,在汇编指令是这样的:

1
2
mov ax, 1 ;把 1 放入寄存器 ax
add ax, 2 ;用 ax 的内容和 2 相加后存入 ax

而 JVM 的字节码指令是这样的:

1
2
3
iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数相加后出栈,结果入栈

由于操作数栈是内存空间,所以字节码指令不必担心不同机器上寄存器以及机器指令的差别,从而做到了平台无关。

注意,局部变量表中的变量不可直接使用,如需使用必须通过相关指令将其加载至操作数栈中作为操作数使用。比如有一个方法 void foo(),其中的代码为:int a = 1 + 2; int b = a + 3;,编译为字节码指令就是这样的:

1
2
3
4
5
6
7
8
9
iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3
istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中
iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操作数栈
iconst_3
iadd //栈顶的两个数出栈后相加,结果入栈
istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中
return // 方法返回指令,回到调用点

需要说明的是,局部变量表以及操作数栈的容量的最大值在编译时就已经确定了,运行时不会改变。并且局部变量表的空间是可以复用的,例如,当指令的位置超出了局部变量表中某个变量 a 的作用域时,如果有新的局部变量 b 要被定义,b 就会覆盖 a 在局部变量表的空间。

盗用别人的图以让大家对虚拟机栈有个直观的认识(其中小字体 Stack 指的的是虚拟机栈,Frame 是栈帧,Local variables 是局部变量表,Operand Stack 是操作数栈):

虚拟机栈

由虚拟机栈引出的问题

看完上面的代码大家可能会有几点疑惑:什么是 slot?那些指令是什么意思?为什么 a 对应的 slot 的索引值不是从零开始的,它明明是第一个定义的变量啊?

对于这些问题我们一个个来解决。

什么是 slot

首先什么是 slot?slot 是局部变量表中的空间单位,虚拟机规范中有规定,对于 32 位之内的数据,用一个 slot 来存放,如 int,short,float 等;对于 64 位的数据用连续的两个 slot 来存放,如 long,double 等。引用类型的变量 JVM 并没有规定其长度,它可能是 32 位,也有可能是 64 位的,所以既有可能占一个 slot,也有可能占两个 slot。

JVM 字节码指令

第二个问题,那些指令是什么意思?

指令格式

首先我们要理解 Java 指令的格式,Java 的指令以字节为单位,也就是一个字节代表一条指令。比如 iconst_1 就是一条指令,它占一个字节,那么自然 Java 指令不会超过 256 条。实际上 Java 指令目前定义了 200 多条。指令虽然是一个字节,但是它也可以带自己的操作数。JVM 中有这样一条指令 putstatic,其作用是给特定的的静态字段赋值。但是给哪个字段赋值呢?仅仅通过这条指令并不能说明,那么只有通过操作数来指定了。紧跟在 putstatic 后面的两个字节就是它的操作数,这个操作数是一个索引值,指向运行时常量池中该静态字段对应的符号引用。由于符号引用包含了该字段的基本信息,如所属类、简单名称以及描述符,因此 putstatic 指令就知道是给哪个类的哪个字段赋值了。

指令的操作数分两种:一种是嵌入在指令中的,通常是指令字节后面的若干个字节;另一种是存放在操作数栈中的。为了区别,我们把前者叫做嵌入式操作数,把后者叫做栈内操作数。这两者的区别是:嵌入式操作数是在编译时就已经确定的,运行时不会改变,它和指令一样存放于类文件方法表的 Code 属性中;而操作数是运行时确定的,即程序在执行过程中动态生成的。拿 putstatic 指令来说,它有一个嵌入式操作数,该操作数是一个索引值(前面已经提到),它由两个字节组成,紧跟在 putstatic 对应的字节之后;同时它还有一个栈内操作数,位于操作数栈的栈顶,这个操作数就是要赋给静态字段的值,其对应的字节数根据静态字段的类型决定。如果静态字段的类型是 short、int、boolean、char 或者 byte,那么这个操作数就必须是 int 类型,即由栈顶的 4 个字节组成;如果是 float、double 或者 long 类型,那么操作数就是相应的类型,即由栈顶的 4 个、8 个 或者 8 个 字节组成;如果静态字段是引用类型,那么这个操作数的类型也必须是引用类型,即由栈顶的 8 个字节组成。

再举一个例子。iconst_<i> 代表了一个指令族,它的意思是把整数 i 放入操作数栈中,i 的范围是(m1, 0, 1, 2, 3, 4, 5),其中 m1 代表的是 -1。注意,这里的 i 并不是指令的操作数(即非嵌入式操作数,也非栈内操作数),如 iconst_1、iconst_2 和 iconst_3 都是由一个字节组成的字节码指令。我们可以把 i 可以看作是指令的 “隐含操作数”,即指令本身就蕴含了操作数。如果整数 i 超过 [-1, 5] 这个范围,就不能用 iconst_<i> 表示了,因为仅一个字节的字节码指令不可能蕴含所有的整数。此时就需要 bipush 这条指令了,这条指令有一个嵌入式操作数,由一个字节组成,用来表示要放入栈顶的那个整数,该整数放入栈顶时通过扩展符号位变为 32 位的整型。但是一个字节也表示不了所有的整数,如果整数值超过一个字节所能表示的范围,就只能通过 ldc 这条指令了,这条指令带有一个字节的嵌入式操作数,它代表的是一个指向运行时常量池中 Constant_Integer_info 类型常量的索引,通过索引的方式引用运行时常量池中的整数,再大的整数也不怕了。

阅读指令文档

授之以鱼不如授之以渔,在这里不可能将所有的指令都讲解一番,因此我和大家分享一下如何阅读 oracle 官网关于字节码指令的文档吧。文档的地址是:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

我们拿 astore 指令来说: 关于它的文档描述如下:

说明和翻译:

  • 第一行的粗体字是指令的名称;
  • Operation 是指令的功能:把引用存入本地变量中;
  • Format 是指令的格式:它的第一个字节是指令,名称为 astore,第二个字节是指令的嵌入式操作数,名称为 index;Forms 指的是指令的十进制(十六进制)码,astore 的十进制(十六进制)码是 58(0x3a);
  • Operation Stack 是指令执行前后的操作数栈的状态:第一行代表的是指令执行前操作数的状态,第二行是指令执行后操作数栈的状态,箭头是栈顶方向。astore 执行前栈顶是对象引用 objectRef,它是 astore 的栈内操作数,执行后 objectRef 被弹出并存入局部变量表中;
  • Description 是对这条指令的描述:index 是无符号字节,这个 index 必须指向当前栈帧的局部变量表的某个位置。操作数栈的栈顶的那个引用值必须是 returnAddress(方法返回地址)或者是 reference (对象引用)。这个引用会被弹出,其值会被存入局部变量表中索引为 index 的 slot 中;
  • Notes 是注意事项:实现 Java 中的 finally 子句时,astore 指令使用的操作数类型是一个 returnAddress,与 astore 对应的 aload 指令(将局部变量表的的引用值压栈)不能将类型为 returnAddress 类型的值加载到操作数栈,而只能是 reference 类型。aload 和 astore 这种不对称的设计是有意而为之的。astore 指令可以和 wide 指令配合使用以用无符号双字节类型的索引来获取局部变量表中的变量。

局部变量表的第一个变量

从 Java 语言的层面讲,静态方法和实例方法的本质区别在于是否是对象所共享的。而从 JVM 的角度来看,方法(无论静态方法还是实例方法)其实都是对象共享的,实例变量才是对象私有的。对 JVM 而言,静态方法和实例方法的本质区别在于是否需要和具体对象关联:静态方法可以通过类名来调用,它不需要和具体对象关联;而实例方法必须通过对象来进行调用,它需要和具体对象关联。那么,实例方法和具体对象是如何产生关联的呢?其实很简单,编译器在编译时会将方法接收者作为一个隐含参数传入该实例方法,这个参数在方法中有一个很熟悉的名字,叫做 “this”。之所以实例方法可以访问该类的实例变量和其它实例方法,正是因为它有 “this” 这个隐含参数。举个例子,类 A 中的某个方法 b 需要访问实例变量 x,由于实例变量是对象私有的,如果 b 是静态方法,由于它没有具体对象的引用,它并不知道该访问哪个对象的实例变量 x;如果 b 是实例方法,通过隐含参数 this 就能确定要访问的实例变量是 this.x。那么,为什么静态方法也不能调用该类的实例方法呢?本质原因也是没有 this 引用。因为调用实例方法的前提是要传入一个隐含参数,实例方法本来就有这个引用,所以能够把它作为隐含参数传入另一个实例方法;静态方法没有 this 引用,无法给实例方法提供指向方法接收者的隐含参数,因此不能调用实例方法。

如果看懂了上面说的那些,第三个问题也就迎刃而解了。因为我们定义的方法是 void foo(),它是实例方法,因此会有一个指向具体对象的隐含参数 this,this 就存放在局部变量表的第一个位置,即存放在索引为 0 的 slot 中,又由于它的作用域从方法开始一直到方法结束,因此它在局部变量表中的位置不会被其他变量覆盖,从而使得我们在方法中定义的变量只能放在局部变量表后面的位置中。需要注意的是,如果方法有参数(非隐含参数),那么参数会按顺序紧接着 this 存放在局部变量表中,由于参数作用域也是整个方法体,所以方法中定义的局部变量就只能放在参数后面了。总的来说局部变量表中变量的存放顺序为: this(如果是实例方法)=> 参数(如果有的话)=> 定义的局部变量(如果有的话)。

-----------------------------------------------------------------------------------------------------------------------------------------------------

虚拟机栈

    其中,虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。如下图所示: 栈流程

栈帧

    操作数栈,动态连接,方法返回地址和一些额外的附加信息。 如下图所示:

栈帧详情

1.局部变量表

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

2.操作数栈

    操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个“字宽”占4个字节,64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。 另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。

3.动态连接

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

4.方法返回地址

    当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。     无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

5.附加信息

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


查看评论

深入理解Java虚拟机--运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。        在编译程序代码时,栈...
  • u010010428
  • u010010428
  • 2016-07-16 10:22:33
  • 1676

java虚拟机栈

一、java虚拟机栈(java virtual machine stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈...
  • cattle26
  • cattle26
  • 2013-01-17 11:47:35
  • 444

基于栈虚拟机和基于寄存器虚拟机的比较

基于寄存器的虚拟机和基于栈的虚拟机有什么不同?
  • u012481172
  • u012481172
  • 2016-03-16 14:04:34
  • 3319

虚拟机栈与本地方法栈

栈区: 栈中分配的是基本类型和自定义对象的引用。每个线程包含一个栈区,栈中只保存基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈...
  • weixin_38070406
  • weixin_38070406
  • 2017-09-02 21:02:20
  • 426

JAVA 虚拟机栈

java 虚拟机栈,在此规定了两种异常情况:  1.线程请求栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;  2.如果虚拟机栈可以动态扩展...
  • kevinmoto
  • kevinmoto
  • 2012-08-07 20:26:47
  • 717

虚拟机栈和本地方法栈溢出

由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但 实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地...
  • pfnie
  • pfnie
  • 2016-10-09 15:16:29
  • 1010

JVM之虚拟机栈

虚拟机栈是一种可以被用来快速访问的存储区域,该区域位于通用RAM[1]里面,通过使用它的所谓的“栈指针”可以让我们访问处理器。栈是一种快速有效的分配存储方法,存取速度仅次于寄存器,堆栈指针若向下移动,...
  • zmycoco2
  • zmycoco2
  • 2017-11-21 11:20:06
  • 104

深入理解java虚拟机(十) Java 虚拟机运行时栈帧结构

栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。 每一个栈帧在编译程序代码的时候...
  • zq602316498
  • zq602316498
  • 2014-08-29 15:52:52
  • 7380

深入理解Java虚拟机笔记---运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作...
  • xtayfjpk
  • xtayfjpk
  • 2014-12-14 12:40:59
  • 6273

JVM中的本地方法栈(Native Method Stacks)和Java虚拟机栈(Java Virtual Machine Stacks)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native...
  • uk8692
  • uk8692
  • 2016-02-17 14:19:29
  • 1984
    个人资料
    持之以恒
    等级:
    访问量: 500
    积分: 277
    排名: 155万+
    文章存档
    最新评论