在上一篇Java 对象在内存文章中我们了解了对象是如何在堆中存放的.
今天我们在一起来了解下JVM中的栈
栈是JVM内存区域中非常重要的一个区域, JVM会对每个线程创建一个栈, 在线程销毁时,释放栈空间.每个栈又是由多个栈帧组成.
栈帧:
一个栈中可以有多个栈帧, 栈帧是随着方法的调用而创建, 随着方法的结束而销毁.
栈帧的主要组成部分:
1. 局部变量表: 存储方法参数和局部变量的存储空间.
2. 操作数栈: 方法执行过程中, 通过字节码push/pop操作, 进行算术运算或者是调用其他方法等操作.
通常两个栈帧是相互独立的,但是大多数虚拟机的实现都会进行优化,令两个栈帧出现部分重叠,减少参数的传递等操作.
3. 动态连接: 指向运行时常量池的方法引用.
4. 方法返回地址: 方法调用者中栈帧地址.
以如下代码为例,看下JVM是如何使用栈的
代码语言:javascript复制public class User {
public static void main(String[] args) {
add(1, 2);
}
public static int add(int i1, int i2) {
int result = 0;
result = i1 i2;
return result;
}
}
利用Javap 命令,将class文件转成字节命令
代码语言:javascript复制javap -p -l -c -s -verbose User.class
我们看下User类中add()方法的字节命令,分析下栈的使用情况
代码语言:javascript复制public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn
LineNumberTable:
line 10: 0
line 11: 2
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 i1 I
0 8 1 i2 I
2 6 2 result I
栈帧操作
1. 栈帧空间
add()方法对应栈帧的操作数栈深度为2,局部变量表中共3个变量,方法参数共2个
由此可见一个方法对应的栈帧空间是固定并且可预期的
代码语言:javascript复制Code:
stack=2, locals=3, args_size=2
2. 字节命令执行
根据LineNumberTable,可知每行代码分别对应了哪些字节命令;
根据这些字节命令,就能知道一行代码在执行过程中是如何利用栈帧中的不同空间进行运算执行的了;
程序计数器也是根据LineNumberTable,知道当前线程执行到了哪行代码.
字节命令执行过程如下:
代码行表 | 源码 | 对应字节命令 | 备注 |
---|---|---|---|
line 10: 0 | int result = 0; | 0: iconst_0 | 将int型(0)压入操作栈顶 |
1: istore_2 | 将操作数栈栈顶值压入局部变量表第2个变量(result)中 | ||
line 11: 2 | result = i1 i2; | 2: iload_0 | 将局部变量表中第0个变量(参数i1)压入操作数栈 |
3: iload_1 | 将局部变量表中第1个变量(参数i2)压入操作数栈 | ||
4: iadd | 从操作数栈中弹出2个变量,进行加运算,并将结果压入栈 | ||
5: istore_2 | 将操作数栈栈顶值(加运算结果)存入局部变量表中第二个变量(result)中 | ||
line 12: 6 | return result; | 6: iload_2 | 将局部变量表中第2个变量(result)压入操作数栈 |
7: ireturn | 将栈顶值写入方法返回地址方法结束,栈帧回收. |
3. 栈
每个方法调用时都会创建1个栈帧
Main()方法在调用add()方法时,栈内结构大致如下:
通过add()方法的执行过程,可以清晰的说明字节命令是如何利用栈执行代码的.
我们可以抽象的理解为JVM栈代表了处理逻辑, 而JVM堆代表了数据.