0%

虚拟机栈

虚拟机栈

虚拟机栈(Java Virtual Machine Stack),每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧(Stack Frame),对应着一次次的方法调用,生命周期与线程一致,也是线程私有的,其描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到执行完成的过程就对应着一个栈帧在虚拟机中入栈到出栈的过程

虚拟机栈的访问速度是很快的,仅次于程序计数器,JVM对于虚拟机栈的操作只有两个入栈(方法执行)和出栈(方法结束)

虚拟机栈并不存在垃圾回收

Java栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入java栈,每一个函数调用结束(return或者抛出异常),都会有一个栈帧被弹出

虚拟机栈的存储结构

栈中的数据都是以栈帧进行存储的,在线程上运行的每个方法都对应着一个栈帧,那栈帧里存储的是什么呢

栈帧结构

栈帧中存储着局部变量表操作数栈动态链接方法返回地址一些附加信息,每个方法的调用都伴随着栈帧的入栈操作,方法的返回伴随着出栈操作,如果方法调用时,方法的参数和局部变量相对较多,就会导致局部变量表较大,而且对于方法调用嵌套的情况,如果方法调用一直不出栈,会导致栈空间不足(尤其是递归,层次过深会导致栈空间不足,从而抛出异常)

下面就分别介绍一下这些东西存储的是什么

局部变量表

局部变量表也被称为本地变量表,定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量,局部变量表所需的内存空间在编译期完成分配,保存在方法的Code属性的maximum local variables(或者locals)数据项中。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在运行期间不会改变

局部变量表中的存储单元是变量槽(slot),一个槽位是4个字节,32位以内(byte、short、boolean、char、int、float、引用类型)的占用一个slot,64位的类型(double、long)占用两个连续的slot

1
2
3
4
public static void main(String[] args) {
Class clazz = List.class;
System.out.println(clazz.isAssignableFrom(ArrayList.class));
}

编译出来的class文件中的字节码(使用javap -v来查看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class java/util/List
2: astore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: ldc #4 // class java/util/ArrayList
9: invokevirtual #5 // Method java/lang/Class.isAssignableFrom:(Ljava/lang/Class;)Z
12: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
15: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
3 13 1 clazz Ljava/lang/Class;

locals就表示局部变量表的大小,在LocalVariableTable中就是将这两个局部变量列出来

1
2
3
public void test(){
System.out.println("");
}

注意看一下实例方法,实例方法内是有一个this的局部变量声明的,所以实例方法中是可以使用this来调用实例变量的,而且会存放在index为0的slot处,其余参数再继续向后排列。这也是为什么静态方法不可以引用this,因为静态方法的局部变量表中不存在this变量

构造方法中也包含有this的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/zhanghe/study/init/Test1;

如果一个方法的参数和局部变量太多,就会导致局部变量表增大,从而导致栈帧增大,而栈的大小如果是固定的,也就导致了栈内的栈帧会很小,从而导致其嵌套次数就必须随之减少

在方法执行结束后,局部变量表会随着方法栈帧的销毁而销毁

在该区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,当扩展时我要发申请到足够的内存,会抛出OutOfMemoryError异常
操作数栈

操作数栈,使用数组的形式来实现的栈(LIFO 后进先出),在方法执行过程中,根据字节码指令,需要往栈中写入或提取数据,即入栈(push)和出栈(pop),而操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时存储空间

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,在编译期进行确定,保存在Code属性中的max_stack(stack),32位类型占用一个栈单位深度,64位占用两个栈单位深度

1
2
3
public void test1(){
double m = 200.0;
}

可以看到仅有一个double时栈深度是2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc2_w #9 // double 200.0d
3: dstore_1
4: return
LineNumberTable:
line 27: 0
line 28: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhanghe/study/init/Test1;
4 1 1 m D

1
2
3
public void test1(){
int i = 1000;
}

而只有一个int时栈深度只有1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: sipush 1000
3: istore_1
4: return
LineNumberTable:
line 25: 0
line 28: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhanghe/study/init/Test1;
4 1 1 i I

动态链接

动态链接、方法返回地址以及一些附加信息又被统称为帧数据区

每一个栈帧内部都包含了一个指向运行时常量池中该栈帧所属方法的引用,目的是为了支持当前方法的代码能够实现动态链接,如invokedynamic指令,在java编译为字节码时,所有的变量和方法引用都是作为符号引用保存在class文件的常量池中,一个方法若要调用其他方法或者访问成员变量,都需要通过符号引用来表示,而动态链接就是为了将这些符号引用转换为调用方法的直接引用

字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。

  • 这些符号引用一部分会在类加载的解析阶段(方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的)或第一次使用的时候被转化为直接引用,这种转化被称为静态解析。主要是静态方法和私有方法两大类,这两种方法决定了它们都不可能通过继承或别的方式重写出其他版本,都适合在类加载阶段进行解析。

  • 另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态链接。

字节码中的常量池(#1、#2等就是符号引用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = Class #31 // java/util/List
#3 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #34 // java/util/ArrayList
#5 = Methodref #35.#36 // java/lang/Class.isAssignableFrom:(Ljava/lang/Class;)Z
#6 = Methodref #37.#38 // java/io/PrintStream.println:(Z)V
#7 = String #39 //
#8 = Methodref #37.#40 // java/io/PrintStream.println:(Ljava/lang/String;)V
#9 = Class #41 // com/zhanghe/study/init/Test1
#10 = Class #42 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/zhanghe/study/init/Test1;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 clazz
#23 = Utf8 Ljava/lang/Class;
#24 = Utf8 test
#25 = Utf8 test1
#26 = Utf8 i
#27 = Utf8 I
#28 = Utf8 SourceFile
#29 = Utf8 Test1.java
#30 = NameAndType #11:#12 // "<init>":()V
#31 = Utf8 java/util/List
#32 = Class #43 // java/lang/System
#33 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#34 = Utf8 java/util/ArrayList
#35 = Class #46 // java/lang/Class
#36 = NameAndType #47:#48 // isAssignableFrom:(Ljava/lang/Class;)Z
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(Z)V
#39 = Utf8
#40 = NameAndType #50:#52 // println:(Ljava/lang/String;)V
#41 = Utf8 com/zhanghe/study/init/Test1
#42 = Utf8 java/lang/Object
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/lang/Class
#47 = Utf8 isAssignableFrom
#48 = Utf8 (Ljava/lang/Class;)Z
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Z)V
#52 = Utf8 (Ljava/lang/String;)V

一个普通的方法

1
2
3
public void test(){
System.out.println("");
}

编译之后生成的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 21: 0
line 22: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/zhanghe/study/init/Test1;


可以看到有些后面

1
2
3
0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
方法返回地址

方法返回地址存储的是调用该方法的程序计数器的值,方法退出实际上是当前栈帧出栈的过程,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈栈帧的操作数栈、设置程序计数器等,让调用者方法继续执行

java方法有两种返回的方式:一种是正常返回,即return;另一种是抛出异常

一些附加信息

栈帧中允许存在与java虚拟机实现相关的一些附加信息,可能有可能没有

虚拟机栈中可能出现的异常

虚拟机栈的大小可以是固定大小也可以是动态的

  • 如果设置为固定大小的话,那么每一个线程的虚拟机栈容量在线程创建的时候就指定了,而此时如果线程请求分配的容量超过了虚拟机栈允许的最大容量,就会抛出StackOverflowError异常
  • 如果使用的是动态大小的话,在尝试进行扩展的时候无法申请到足够的内存,或者在创建线程的时候没有足够的内存去创建对应的虚拟机栈,那么就会抛出OutOfMemoryError异常

可以使用-Xss来设置线程的最大栈空间

欢迎关注我的其它发布渠道