荐 【探究JVM四】Java方法执行的线程内存模型——虚拟机栈 字节码指令追踪,万字长文深入探究内部结构
文章目录
1. 虚拟机栈概述
虚拟机栈出现背景
由于跨平台性的设计,Java指令都是基于栈(8位对齐)来设计的。不同平台的CPU架构不同,所以不能设计为基于寄存器的(16位为对齐)。
【基于栈的优势】:跨平台,指令集小,编译器容易实现;
【劣势】:性能下降,实现的同样的功能指令多
内存中的栈与堆
栈是运行时的单位,堆是存储时的单位。
-
虚拟机栈解决程序的运行问题,程序执行最终都是压入虚拟机栈的栈帧中
-
堆解决的是数据存储问题,(除去基本类型之外)主体数据均是存储在堆上
就好比是我们电脑的内存和硬盘,程序的执行都需要加载到内存中来运行。但是保存数据都是持久化在硬盘上的,硬盘中的数据需要读取到内存中才能执行。
虚拟机栈的基本内容
【作用】:管理Java程序的运行,保存方法的局部变量、部分结果,参与方法的调用和返回
【生命周期】:与线程的生命周期相同
【特点】
-
访问速度仅次于PC计数器(只涉及到入栈和出栈的操作)
-
不存在垃圾回收问题
会存在OOM和*
出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者固定不变的
《Java虚拟机规范》中规定了两类异常状况:
-
如果线程请求的栈的深度大于虚拟机所允许的最大深度,抛出
*Error
-
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那JVM将会抛出
OutOfMemoryError
异常
HotSpot虚拟机栈容量不可以动态扩展,但是申请失败是仍会产生OOM
【手动设置虚拟机栈的容量大小】
-
-Xss256k
(单位为k、m、g,忽略大小写)
2. 虚拟机栈的内部结构
栈中存储什么
虚拟机栈的基本单元是栈帧(Stack Frame),一个栈帧对应一个Java方法的调用
【执行原理】
由于虚拟机栈是线程私有的,不同的线程有不同的栈,不同的栈之间数据不能共享。即不可能存在一个栈帧中引用另外一个线程的栈帧。它们可以通过同一个进程来共享数据。
Java方法有两种返回函数的方式:
-
一种是正常的函数返回,使用 return指令;
-
另外一种是抛出异常。
不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部数据
每个栈帧中存储着5部分数据:
-
局部变量表(Local Variables)
-
操作数栈(Operand Stack)
-
动态链接(Dynamic Linking)
-
方法返回地址(Return Address)
-
附加信息
接下来,我们分别讨论这5部分的作用
3. 局部变量表
局部变量表Local Variables(局部变量数组或者本地变量表)。
-
局部变量表是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。
- 数据类型包括:基本数据类型、引用类型、返回值类型
-
线程私有数据,不存在线程安全问题
-
局部变量表的容量大小在编译期间被确定,在方法运行期间不会改变
使用 jclasslib插件查看out包下反编译后的字节码文件
main方法对应的字节码文件
0 new #1 <iqqcode/jvmstack/LocalVariablesTest01>
3 dup
4 invokespecial #2 <iqqcode/jvmstack/LocalVariablesTest01.<init>>
7 astore_1
8 bipush 10
10 istore_2
11 aload_1
12 invokevirtual #3 <iqqcode/jvmstack/LocalVariablesTest01.test1>
15 return
package iqqcode.jvmstack;
import java.util.Date;
/**
* @Author: Mr.Q
* @Date: 2020-06-25 09:39
* @Description:局部变量表容量测试
*/
public class LocalVariablesTest01 {
private int count = 0;
public static void main(String[] args) {
LocalVariablesTest01 test = new LocalVariablesTest01();
int num = 10;
test.test1();
}
public static void testStatic(){
LocalVariablesTest01 test = new LocalVariablesTest01();
Date date = new Date();
int count = 10;
System.out.println(count);
//因为this变量不存在于当前方法的局部变量表中!!
//System.out.println(this.count);
}
//关于Slot的使用的理解
public LocalVariablesTest01(){
this.count = 1;
}
public void test1() {
Date date = new Date();
String name1 = "iqqcode";
test2(date, name1);
System.out.println(date + name1);
}
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "Mr.Q";
double weight = 130.5;//占据两个slot
char gender = '男';
return dateP + name2;
}
public void test3() {
this.count++;
}
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//变量c使用之前已经销毁的变量b占据的slot的位置
int c = a + 1;
}
public void test5Temp() {
int num;
//System.out.println(num);//错误信息:变量num未进行初始化
}
}
槽Slot:
数字数组中存放法的数据类型包括:基本数据类型(boolean、byte、char、short、int、float、long、double)、引用类型(reference引用指针类型)、返回值类型(returnAddress指向字节码指令的地址)
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
局部变量表和操作数栈底层都是采用数组实现的,所以一旦初始化后,长度是固定不变的
这些数据类型在局部变量表中的存储空间用局部变量槽(Slot)来表示,long
和double
是64位的占两个槽,其余占一个。
所以局部变量表的容量就是槽的个数,在编译期间就是确定的
变量的分类:
一、按照数据类型分:① 基本数据类型 ② 引用数据类型
二、按照在类中声明的位置分:
① 成员变量:在使用前,都经历过默认初始化赋值
-
类变量: Linking的
Prepare
阶段:给类变量默认赋值 ====> initial阶段:给类变量显式赋值 -
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
4. 操作数栈
栈帧中存储的第二部分数据----操作数栈Operand Stack(也称表达式栈)。
主要用来保存计算过程的中间结果,同时作为计算过程中 变量临时的存储空间。
操作数栈对于数据的存储跟局部变量表是一样的,但是跟局部变量表不同的是,操作数栈对于数据的访问不是通过下标,而是通过标准的栈操作来进行的(压入与弹出)
对于数据的计算是由CPU完成的,所以CPU在执行指令时每次会从操作数栈中弹出所需的操作数,经过计算后再压入到操作数栈顶。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
我们通过一个例子来进行代码追踪分析:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
反编译之后的主体代码:
{
public iqqcode.pcregister.PCRegisterTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Liqqcode/pcregister/PCRegisterTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 i I
6 5 2 j I
10 1 3 k I
}
SourceFile: "PCRegisterTest.java"
我们对代码进行跟踪分析
对照字节码指令,我们逐行分析:
初始情况
I. 根据PC计数器记录的代码偏移位置0,操作数10入栈
II. 根据PC计数器记录的代码偏移位置2,操作数10出栈放入到局部变量表中索引为1的位置处
III. 根据PC计数器记录的代码偏移位置3,操作数20入栈
IV. 根据PC计数器记录的代码偏移位置5,操作数20出栈放入到局部变量表中索引为2的位置处
V. 从局部变量表中取出索引1位置的数(10),放入到操作数栈中
VI. 从局部变量表中取出索引2位置的数(20),放入到操作数栈中
VII. 将两个操作数出栈相加后加过再入栈
VIII. 操作数30放入局部变量表索引为3的位置处
IX. 方法结束返回(返回为空)
5. 动态链接
动态链接、方法返回地址、附加信息统称为帧数据区
动态链接Dynaminc Linking(或指向运行时常量池的方法引用)。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Sysmbolic Reference) 保存到class文件的 常量池中。描述一个方法调用用了其他方法时,就是通过常量池中指向方法的符号引用来表示的。
动态链接的作用就是为了将符号引用转换为直接引用的
这不就和类加载系统中链接阶段的解析Resolve过程类似么