目录
虚拟机栈(JVM Stack)的介绍
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储 局部变量表、操作数栈、动态连接、方法出口 等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出 StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
虚拟机栈主要用于存储四部分内容
【局部变量表】、【操作数栈】、【动态连接】和【方法返回地址】
1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;
(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
3. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。
对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
栈帧(Stack Frame)
栈帧(Stack Frame)是用于支持虚拟机进行 方法调用 和 方法执行 的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。
栈帧存储了方法的 【局部变量表】、【操作数栈】、【动态连接】和【方法返回地址】等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
在编译程序代码的时候,栈帧中 需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
栈执行结构图
在活动线程中,只有位于栈顶的栈帧才是有效的,称为【当前栈帧】,与这个栈帧相关联的方法称为【当前方法】。
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
局部变量表的结构:
Slot复用 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的, 也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕), 那这个变量对应的Slot就可以交给其他变量使用。 优点 : 节省栈帧空间。 缺点 : 影响到系统的垃圾收集行为。 (如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
1.局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。 并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,
对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
!!!!! 很多人说:基本数据和对象引用存储在栈中。
当然这种说法虽然是正确的,但是很不严谨,只能说这种说法针对的是局部变量。
局部变量存储在局部变量表中,随着线程而生,线程而灭。并且线程间数据不共享。
但是,如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中。
因为在堆中,是线程共享数据的,并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表!
reference(对象实例的引用)
一般来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 :
a.在Java堆中的数据存放的起始地址索引。
b.所属数据类型在方法区中的存储类型。
例如:我们在创建一个Student对象时的数据存储结构:
操作数栈
操作数栈也常被称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了,重叠过程如下图:
通过一段代码来了解操作数栈:
public class OperandStack{
public static int add(int a, int b){
int c = a + b;
return c;
}
public static void main(String[] args){
add(100, 98);
}
}
使用 javap 反编译 OperandStack 后,根据虚拟机指令集,得出操作数栈的运行流程如下:
add 方法刚开始执行时,操作数栈是空的。当执行 iload_0 时,把局部变量 0 压栈,即 100 入操作数栈。然后执行 iload_1,把局部变量1压栈,即 98 入操作数栈。接着执行 iadd,弹出两个变量(100 和 98 出操作数栈),对 100 和 98 进行求和,然后将结果 198 压栈。然后执行 istore_2,弹出结果(出栈)。
下面通过一张图,对比执行100+98操作,局部变量表和操作数栈的变化情况。
动态连接
每个栈帧都包含一个指向运行时 常量池中(运行时常量池(Runtime Constant Pool)是方法区的一部分。) 该栈帧所属方法的引用,
持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。
另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。
为什么需要常量池?
A:字节码文件中需要很多数据的支持,但数据很大,不能直接保存到字节码文件中,所以常量池的作用就是为了提供一些符号和常量,便于指令的识别。
方法返回地址
一个方法的结束有两种方式:
正常执行结束
出现未处理的异常,非正常退出
当一个方法开始执行以后,只有两种方法可以退出当前方法:
当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
当方法返回时,可能进行3个操作:
恢复上层方法的局部变量表和操作数栈
把返回值压入调用者调用者栈帧的操作数栈
调整 PC 计数器的值以指向方法调用指令后面的一条指令
Return:
当一个方法开始执行后,执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,称正常完成出口
字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short、int类型时使用)lreturn、freturn、dreturn、areturn(return指令供声明void的方法、实例初始化方法、类和接口的初始化方法使用)。
在方法中遇到异常(Expection):
并且这个异常没有在方法内进行处理,也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口
方法在执行过程中抛出异常时的异常处理存储在一个异常处理表方便在处理异常时找到处理的对应代码
使用 javap 反编译 OperandStack
1. 首先你已经创建了OperandStack.java 文件
2. 电脑上已经有了jdk的运行环境, 通过cmd对 OperandStack.java 文件进行编译:
指令 D:\gaoeclipselearning\hcgao\learning\javap> javac OperandStack.java
3.此时已经生成了class文件,对OperandStack.class 文件进行反编译查看:
指令 D:\gaoeclipselearning\hcgao\learning\javap> javap -c -l OperandStack > test.txt
4.查看内容:
Compiled from "OperandStack.java"
public class com.hcgao.common.util.learning.javap.OperandStack {
public com.hcgao.common.util.learning.javap.OperandStack();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 5: 0
line 6: 4public static void main(java.lang.String[]);
Code:
0: bipush 100
2: bipush 98
4: invokestatic #2 // Method add:(II)I
7: pop
8: return
LineNumberTable:
line 10: 0
line 11: 8
}
下面来一个图例:
执行 add(1,2) 的过程,最后 ireturn 会将操作数栈栈顶的值返回给调用者
javap的用法格式:
javap <options> <classes>
其中classes就是你要反编译的class文件。
在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:
-hep --hep -? 输出此用法消息
-version 版本信息,其实是当前javap所在jdk的版本信息,不是cass在哪个jdk下生成的。
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
- 输出行号和本地变量表
-pubic 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类 和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示静态最终常量
-casspath &t;path> 指定查找用户类文件的位置
-bootcasspath &t;path> 覆盖引导类文件的位置
一般常用的是-v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。
参考:《深入理解Java虚拟机第二版》、《Java虚拟机规范 JavaSE8版》
https://blog.csdn.net/rongtaoup/article/details/89142396
https://blog.csdn.net/w372426096/article/details/81664431
https://blog.csdn.net/u014296316/article/details/82668670