虚拟机栈简介
虚拟机栈是JVM运行时数据区的一个重要部分,他是线程私有的,同时可能存在StackOverFlowError
和OutOfMemoryError
。 StackOverFlowError
出现在栈帧花费完栈内存容量时发生,OutOfMemoryError
出现在动态扩展栈容量时无内存供其扩展时发生。 我们可以在运行时指定 -Xss 容量
指定本次JVM中的栈容量大小。
栈帧简介
对于栈中的内容,都为一个一个的栈帧,栈帧随着方法的调用而创建或销毁,每一个栈帧都是用于描述运行中的方法的一些状态数据。通常认为栈帧中存储的内容主要分为五个部分:
- LocalVariableTable 局部变量表
- Operand stack 操作数栈
- Dynamic Linking 动态链接
- Return Adress 方法返回地址
- 其他数据区
LocalVariableTable
局部变量表代表着本次方法中需要用到的局部变量,其操作时存入的位置便是我们的局部方法表。 对于局部方法表,有以下几个特点:
- 局部变量表的大小在编译期间就已经确定了
- 局部变量表槽位大小通常为32位,因此double long需要占用两个槽
- 构造函数或对象方法中存在隐含的this局部变量
- 局部变量的槽可复用
我们可以看以下示例:
public class Stack_LocalVariableTable {
public Stack_LocalVariableTable(int a){
double b = 10;
{
char c = 'a';
c++;
}
int d = 20;
}
}
他的字节码文件中关于局部变量表的解析如下:
LocalVariableTable:
Start Length Slot Name Signature
12 7 4 c C
0 24 0 this Lcom/linmu/Stack_LocalVariableTable;
0 24 1 a I
8 16 2 b D
23 1 4 d I
其中Start代表该局部变量作用域起始PC位置, Length代表作用域持续PC数量, Slot代表槽位索引, Name代表变量名,Signature代表类型。
我们可以看到符合我们上面的几个特点:
- 存在隐含的this局部变量
- c的槽位被后来的d重复使用
- double数据的局部变量占用槽位为2
Operand stack
JVM中的操作数栈是用类数组结构实现的,当我们需要对数据进行操作时,都是将数据存入操作数栈再进一步进行操作。 和局部变量表一样,操作数栈大小也是在编译期间就被确定了,我们来看一个示例:
public static int sum(int a, int b){
int c = a + b;
return c;
}
public static int sum(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 33: 0
line 34: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 a I
0 6 1 b I
4 2 2 c I
可以看到,操作数栈大小为2, 并且简单的查阅字节码指令我们也能发现,方法首先将参数(槽位0和1的形参值)load到操作数栈中,然后使用add将操作数栈中的两个操作数相加,并且将其store:取出并保存在槽位为2的局部变量表中。 随后再将槽位为2的变量值取出,并且返回。
Dynamic Linking
在jdk1.7前,字节码文件中有关方法调用的指令只存在以下几种:
- invokestatic
- invokespecial
- invokevirtual
- invokeinterface
在jdk1.7时引入了一个新的方法指令:
- invokedynamic
这个指令在jdk1.8的lambda表达式出现后才会自然的出现在我们编译后的字节码文件中。
简单介绍完方法指令,我们接下来说说java中虚函数的含义,学习过c++的同学都知道, c++中期望实现方法调用的多态时,需要声明方法为虚方法,并且子类重写虚方法才能实现。 我们这里的虚方法与其有相似之处也有不同之处:
在java中,如果无法在编译期确定的方法即为虚方法,例如对于static方法、本类方法、指定父类方法等等都是非虚方法。 而那些有多态性质的方法调用: 例如接口、抽象类的对象的方法调用便为我们的虚方法。
虚拟机栈中,对于方法调用需要存在一个动态链接,他决定了我们的虚方法具体需要指向的方法。如以下示例:
public class Stack_DynamicLinking {
interface Person{
void talk();
void eat();
void sleep();
}
class Father implements Person{
@Override
public void talk() {}
@Override
public void eat() {}
@Override
public void sleep() {}
public void laugh(){}
}
class Son extends Father{
@Override
public void laugh() {}
}
public static void main(String[] args) {
Person person = new Stack_DynamicLinking().new Son();
person.eat(); //虚方法
Father father = (Father) person;
father.laugh(); //虚方法
Son son = (Son) father;
son.laugh(); //非虚方法
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: new #2 // class com/linmu/Stack_DynamicLinking$Son
3: dup
4: new #3 // class com/linmu/Stack_DynamicLinking
7: dup
8: invokespecial #4 // Method "<init>":()V
11: dup
12: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: pop
16: invokespecial #6 // Method com/linmu/Stack_DynamicLinking$Son."<init>":(Lcom/linmu/Stack_DynamicLinking;)V
19: astore_1
20: aload_1
21: invokeinterface #7, 1 // InterfaceMethod com/linmu/Stack_DynamicLinking$Person.eat:()V
26: aload_1
27: checkcast #8 // class com/linmu/Stack_DynamicLinking$Father
30: astore_2
31: aload_2
32: invokevirtual #9 // Method com/linmu/Stack_DynamicLinking$Father.laugh:()V
35: aload_2
36: checkcast #2 // class com/linmu/Stack_DynamicLinking$Son
39: astore_3
40: aload_3
41: invokevirtual #10 // Method com/linmu/Stack_DynamicLinking$Son.laugh:()V
44: return
LineNumberTable:
line 37: 0
line 38: 20
line 40: 26
line 41: 31
line 43: 35
line 44: 40
line 45: 44
LocalVariableTable:
Start Length Slot Name Signature
0 45 0 args [Ljava/lang/String;
20 25 1 person Lcom/linmu/Stack_DynamicLinking$Person;
31 14 2 father Lcom/linmu/Stack_DynamicLinking$Father;
40 5 3 son Lcom/linmu/Stack_DynamicLinking$Son;
}
Return Adress
方法返回地址是最为简单的一个区域, 在每次方法结束后,栈帧都会被弹出,然后进程回到上一个方法的执行中。 返回地址即是标识我们的代码应该从哪儿继续执行。
也就是说,他存储的为PC寄存器中的数据,标识当前方法返回后原方法的执行位置。