0%

虚拟机栈

虚拟机栈

虚拟机栈(Java Virtual Machine Stack),每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧(Stack Frame),对应着一次次的方法调用,生命周期与线程一致,主要用来管理java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回

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

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

虚拟机栈的存储结构

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

栈帧结构

栈帧中存储着局部变量表操作数栈动态链接方法返回地址一些附加信息

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

局部变量表

局部变量表也被称为本地变量表,定义为一个数字数组,主要用于存储方法参数和定义在方法内的局部变量,局部变量表所需的容量大小在编译期就确定下来了,保存在方法的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的局部变量

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;

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

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

操作数栈

操作数栈,使用数组的形式来实现的栈,在方法执行过程中,根据字节码指令,需要往栈中写入或提取数据,即入栈(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虚拟机实现相关的一些附加信息,可能有可能没有

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

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

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

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