JVM 栈帧
一、栈帧
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。
局部变量表和操作数栈的容量是在编译期确定,并通过方法的Code属性保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存。
在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。
栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。
二、局部变量表
每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。
一个局部变量(Slot)可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。
局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。
long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。例如我们讲一个double类型的值存储在索引值为n的局部变量中,实际上的意思是索引值为n和n+1的两个局部变量都用来存储这个值。索引值为n+1的局部变量是无法直接读取的,但是可能会被写入,不过如果进行了这种操作,就将会导致局部变量n的内容失效掉。
上文中提及的局部变量n的n值并不要求一定是偶数,Java虚拟机也不要求double和long类型数据采用64位对齐的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个double或long类型的值。
Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。
三、操作数栈
每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。
在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。
操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
举个例子,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的int型数值。在iadd指令执行时,2个int值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。
每一个操作数栈的成员(Entry)可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。
在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈栈顶的数据类型相匹配,例如不可以入栈两个int类型的数据,然后当作long类型去操作他们,或者入栈两个float类型的数据,然后使用iadd指令去对它们进行求和。有一小部分Java虚拟机指令(例如dup和swap指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过Class文件的校验过程来强制保障。
在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。
分析:
有如下代码:
- package cc.lixiaohui.demo;
- public class Foo {
- public static void main(String[] args) {
- int a = 1;
- int b = 2;
- int c = a + b;
- }
- }
利用javap工具生成虚拟机汇编代码(java虚拟机指令集):
括号内的注释我是自己加的,其余是javap生成的
- E:\EclipseWorkspace\demo-foo\target\classes\cc\lixiaohui\demo>javap -c -l Foo.class
- Compiled from "Foo.java"
- public class cc.lixiaohui.demo.Foo {
- public cc.lixiaohui.demo.Foo(); (//这是默认构造方法)
- Code:
- 0: aload_0
- 1: invokespecial #8 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 3: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this Lcc/lixiaohui/demo/Foo;
- public static void main(java.lang.String[]);
- Code:
- 0: iconst_1 (//把int值1压入操作数栈)
- 1: istore_1 (//把栈顶值存储到局部变量表下标为1的位置)
- 2: iconst_2 (//把int值2压入操作数栈)
- 3: istore_2 (//把栈顶值存储到局部变量表下标为2的位置)
- 4: iload_1 (//取局部变量表中下标为1的变量压栈)
- 5: iload_2 (//取局部变量表中下标为2的变量压栈)
- 6: iadd (//从操作数栈中弹出两个int值进行相加操作,相加的结果压栈)
- 7: istore_3 (//把栈顶值存储到局部变量表下标为3的位置)
- 8: return
- LineNumberTable: (//这是java代码行与该栈帧指令代码行的映射)
- line 5: 0 (//java代码第五行为“int a = 1”,对应的虚拟机汇编代码为“iconst_1”)
- line 6: 2
- line 7: 4
- line 8: 8
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 9 0 args [Ljava/lang/String;
- 2 7 1 a I
- 4 5 2 b I
- 8 1 3 c I
- }
可以看到执行过程中不断在局部变量表和操作数栈间来回传递数据。
相关推荐
JVM栈帧图示,包括局部变量,操作数栈,动态连接,返回地址等各项描述。
每一个 JVM 线程都拥有一个私有的 JVM 线程栈,用于存放当前线程的 JVM 栈帧(包括被调用函数的参数、局部变量和返回地址等)。如果某个线程的线程栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java....
- 栈帧优化,如逃逸分析,帮助确定对象是否需要在堆上分配。 6. **JVM调优工具**: - JConsole和VisualVM提供可视化的监控和诊断,帮助开发者理解应用程序的运行状况。 - JProfiler和YourKit等商业工具提供更...
9. **栈帧与操作数栈**:每个线程都有一个独立的虚拟机栈,栈帧是方法执行的内存模型,操作数栈用于存储计算过程中的中间结果。 10. **异常处理**:JVM如何处理运行时异常,以及栈展开的过程。 11. **线程并发**:...
- Java虚拟机栈:每个方法对应一个栈帧,存储局部变量表、操作数栈、动态链接和方法出口等信息。 - 本地方法栈:与JVM栈类似,但服务于Java Native Interface(JNI)调用的本地方法。 - 堆内存:存放对象实例,...
每个线程都会有一个程序计数器和栈来跟踪方法调用,栈中存放了栈帧,其中包含了局部变量区和操作数栈。 在Java平台中,JDK(Java Development Kit)是开发Java程序的软件开发包,而JRE(Java Runtime Environment)...
方法栈是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。PC 寄存器用于存储待执行指令的地址,以便线程恢复和跳转。Java 堆是 JVM 用于存放...
当从`main()`函数中调用`Min()`函数时,JVM会创建一个新的栈帧来保存局部变量和参数,并跳转到相应的字节码位置继续执行。 ##### 访问Min()函数中的参数 由于JVM采用栈式架构,访问参数通常涉及从栈顶弹出值并加载...
Java虚拟机(JVM)是Java程序运行的基础,它的调优是提高应用程序性能的关键环节。在JVM调优实践中,了解各个运行时数据区的工作原理至关重要。以下是对这些区域的详细解析: 1. **虚拟机栈**:每个线程都有一个...
### JVM内存空间分配详解 #### 一、JVM内存模型概览 JVM(Java虚拟机)内存模型主要由以下几个部分组成:程序计数器、Java虚拟机栈、本地方法栈、Java堆以及方法区(在JDK 8之后称为元空间)。下面将对这几个部分...
JVM,即Java虚拟机,是Java程序能够运行的运行环境。它在执行Java程序的过程中,负责管理内存和资源,保证程序的正常运行。本文根据《深入理解Java虚拟机》书籍内容及作者理解,总结了JVM相关的知识点,分享如下: ...
每个线程有自己的程序计数器和栈,栈又由一系列栈帧组成,每个栈帧包含局部变量区和操作数栈,用于存储方法的局部变量、参数和执行过程中的中间结果。 在内存管理方面,JVM内存主要分为堆、栈、本地方法栈和方法区...
### 马士兵JVM调优笔记知识点梳理 #### 一、Java内存结构 Java程序运行时,其内存被划分为几个不同的区域,包括堆内存(Heap)、方法区(Method Area)、栈(Stack)、程序计数器(Program Counter Register)以及...
每当一个本地方法被调用,就会在本地方法栈上创建一个新的栈帧。 - **运行时常量池(Runtime Constant Pool)**:存储类文件的常量,包括字符串字面量和符号引用。在Java 8中,运行时常量池也被移到了堆中。 ### 2...
栈帧用于存储局部变量、操作数栈和动态链接信息。 6. 本地方法栈(Native Method Stack):用于支持native方法的执行。 三、垃圾收集(GC) 1. Minor GC:清理新生代内存,主要针对Eden区和一个Survivor区。 2. ...
* 栈帧:用于存储方法的执行状态和局部变量。 * 操作数栈:用于存储操作数和返回值。 堆 堆是一个抽象概念,用于存储对象实例和数组的数据。堆的主要组件包括: * 对象实例:用于存储对象实例的数据。 * 数组:...
栈帧随着方法的调用和返回而创建和销毁。 4. 本地方法栈:与Java方法栈类似,但服务于Java虚拟机的本地(Native)方法。 5. 程序计数器:每个线程都有自己的程序计数器,记录当前线程正在执行的字节码指令地址。 ...
每个方法调用在JVM中都会创建一个栈帧,包含了局部变量表、操作数栈、动态链接和方法返回地址。局部变量表的大小在编译时确定,每个槽位可以存储一个基本类型或一个对象引用。对于基本类型,如int、float等,它们...
### JVM入门必备手册 #### 前言 Java 虚拟机 (JVM) 是 Java 开发者不可或缺的一部分,了解 JVM 的工作原理对于优化应用程序性能至关重要。本手册旨在为初学者提供关于 JVM 的基础知识,重点介绍 JVM 内存模型中的...