简介
虚拟机栈的出现背景
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的【如果设计成基于寄存器的,耦合度高,性能会有所提升,因为可以对具体的CPU架构进行优化,但是跨平台性大大降低】。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
内存中的栈与堆
首先栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里
虚拟机栈基本内容
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的
虚拟机栈的生命周期
生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了
虚拟机栈的作用
主管Java程序的运行,它保存方法的局部变量(8 种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
局部变量,它是相比于成员变量来说的(或属性)
基本数据类型变量 VS 引用类型变量(类、数组、接口)
虚拟机栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对Java栈的操作只有两个:
每个方法执行,伴随着进栈(入栈、压栈)
执行结束后的出栈工作-
对于栈来说不存在垃圾回收问题。栈不需要GC,但是可能存在OOM
虚拟机栈的异常
栈中可能出现的异常?
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError 异常
设置栈内存大小
我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform:
- Linux/x64 (64-bit): 1024 KB
- macOS (64-bit): 1024 KB
- Oracle Solaris/x64 (64-bit): 1024 KB
-
Windows: The default value depends on virtual memory
栈的存储单位
栈中存储什么?
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出(后进先出)原则
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
-
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式。
一种是正常的函数返回,使用return指令。
另一种是方法执行中出现未捕获处理的异常,以抛出异常的方式结束。
但不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
-
一些附加信息
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的
局部变量表
局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress返回值类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题 - 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。
- 对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。
- 进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。
- 在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
- 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
局部变量表(Local Variable Table)是一组变值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在放的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽(Variable Slot)为最小单位。每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference、或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放。Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。
reference类型
reference类型类型表示对一个对象实例的引用。虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般说来,虚拟机实现至少都是通过这个引用做到两点,一是从此引用中直接或间接地查到对象在Java堆中的数据存放地址索引,二是引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束。
它表示了一个对象实例的引用(堆中),主要的作用有两个:
- 能根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址和索引
- 能根据引用直接或间接地查找到对象所属数据类型在元空间种的存储类型信息
64位数据类型(long、double)
对于64位的数据类型,虚拟机以高位对齐的方式为其分配两个连续的Slot空间。Java语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double两种。由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。
变量如何在局部变量表中存储的
通过以上描述,我们知道它是以变量槽的方式进行存储。具体流程我们通过下面代码进行分析
public void show() {
String name = "张三";
int age = 20;
}
对应字节码文件
0 ldc #5 <张三>
2 astore_1
3 bipush 20
5 istore_2
6 return
现在分析字节码文件的执行流程
ldc #5 <张三>
将 张三 的值压栈
astore_1
将 张三 弹出栈并将该值的引用赋予变量槽1的位置。因为字符串是在堆中的,需要引用指向
bipush 20
将 20 压入栈
istore_2
将 20 弹出栈,并将该值赋予变量槽2的位置。因为符合变量槽的要求,所以该值在变量槽中
通过以上得出,String是以引用refrence在变量槽中存放的。int 则是值在变量槽中存放的。
为方便理解,画了一个图:
Slot的访问方式
关于Slot的理解
- 参数值的存放总是在局部变量数组的index 0开始,到 数组长度-1 的索引结束;
- 局部变量表,最基本的存储单元是Slot(变量槽);
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量;
- 在局部变量表里,32位以内的类型只占用一个Slot(包括引用类型),64位的类型(long和double)占用两个Slot;
- byte、short、char在存储前被转换为int,boolean也被转换为int;
- long和double则占据两个Slot。
- JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值;
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上;
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如访问long或double类型变量)
-
如果当前帧是由构造方法或实例方法创建的 ,那么该对象引用this将会存放在index为0的Slot处,其余的参数按照参数表顺序继续排列。
Slot的重复利用(踩坑点)
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
例:在test4()方法中,局部变量个数为3 —》this,a 和 c ,因为变量b只在其作用域中有效,变量b被销毁,但开辟的空间还在,新定义的变量c就占据了这块已开辟的空间。
静态变量与局部变量的对比
变量的分类:
按照数据类型分:
- 基本数据类型
- 引用数据类型
按照在类中声明的位置分:
- 成员变量:在使用前,都会默认初始化赋值。
静态变量(类变量):链接的准备阶段:给类变量默认赋值 —》初始化阶段:再显式赋值。
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。 - 局部变量:在使用前必须进行显式赋值,否则编译不通过。
public static void testTemp1(){
int num1;
System.out.println(num1); //输出0
}
public void testTemp2(){
int num2;
System.out.println(num2); //错误信息:变量num未初始化
}
类变量表有两次初始化的机会,第一次是在链接中的“准备阶段”,执行系统初始化,对类变量设置零值(final修饰的static不会),另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用(编译不会通过)。
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
代码解析
javap -v Demo03.class: 解析class文件
package com.nike.erick.d05;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Demo03 {
public static void main(String[] args) {
}
public Set<String> method01(String address){
List<String> erickList = new ArrayList<>();
double phone = 1234567654;
int age = 100;
return new HashSet<>();
}
public Set<String> method02(String address){
double phone = 1234567654;
int a = 1;
{
int b = a+1;
b = 0;
}
int age = 100;
return new HashSet<>();
}
}
栈帧信息
{
public com.nike.erick.d05.Demo03();
descriptor: ()V
flags: (0x0001) 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 Lcom/nike/erick/d05/Demo03;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
public java.util.Set<java.lang.String> method01(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/util/Set;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=6, args_size=2
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_2
8: ldc2_w #4 // double 1.234567654E9d
11: dstore_3
12: bipush 100
14: istore 5
16: new #6 // class java/util/HashSet
19: dup
20: invokespecial #7 // Method java/util/HashSet."<init>":()V
23: areturn
LineNumberTable:
line 14: 0
line 15: 8
line 16: 12
line 17: 16
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/nike/erick/d05/Demo03;
0 24 1 address Ljava/lang/String;
8 16 2 erickList Ljava/util/List;
12 12 3 phone D
16 8 5 age I
LocalVariableTypeTable:
Start Length Slot Name Signature
8 16 2 erickList Ljava/util/List<Ljava/lang/String;>;
Signature: #34 // (Ljava/lang/String;)Ljava/util/Set<Ljava/lang/String;>;
public java.util.Set<java.lang.String> method02(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/util/Set;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=6, args_size=2
0: ldc2_w #4 // double 1.234567654E9d
3: dstore_2
4: iconst_1
5: istore 4
7: iload 4
9: iconst_1
10: iadd
11: istore 5
13: iconst_0
14: istore 5
16: bipush 100
18: istore 5
20: new #6 // class java/util/HashSet
23: dup
24: invokespecial #7 // Method java/util/HashSet."<init>":()V
27: areturn
LineNumberTable:
line 21: 0
line 23: 4
line 25: 7
line 26: 13
line 28: 16
line 29: 20
LocalVariableTable:
Start Length Slot Name Signature
13 3 5 b I
0 28 0 this Lcom/nike/erick/d05/Demo03;
0 28 1 address Ljava/lang/String;
4 24 2 phone D
7 21 4 a I
20 8 5 age I
Signature: #34 // (Ljava/lang/String;)Ljava/util/Set<Ljava/lang/String;>;
}
SourceFile: "Demo03.java"