开场白: “掷地铿锵嗟有力,杳无声处见刀光。全凭借,三言两语,便道尽,栈结构”——怎么可能?!>_<
栈其实是个很理论的概念。说理论,是因为栈并不指代内存里某块具体的区域,它只是一种数据结构。我们通常讲:某类型数据存放在栈中……这里的栈指的是以栈这种数据结构形式去存放数据的、主存硬件中某块实实在在的区域。
JVM Run-Time Data Areas (运行时数据区)里的这种不同区域的划分是个动态的概念。大致的、通俗的你可以这样理解:内存就是静态的硬件,当 JVM 运行时,静态的内存硬件便会被 JVM 动态地划分为一系列存放各种不同类型数据的区域,这些区域本质上是主存中一块一块的、被 JVM 划分好的物理内存。
这篇文章只讨论 Run-Time Data Areas 的栈结构,这里的栈结构说得确切些应该叫线程栈,为什么叫线程栈,是因为多个线程并发运行时,JVM 会给每一个线程分配不同的栈区域。如图:
接下来我们再来看下面这段代码:
public class Math {
public static int initDate = 666;
public static User user = new User();
public int computes() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
int d = c / b;
return d;
}
public static void main(String[] args) {
Math math = new Math();
int r = math.computes();
System.out.println(r);
}
}
class User {
}
运行结果:
15
使用 javap -c math > math.txt 反汇编得到字节码指令(也可使用开发工具上自带的反汇编工具 ):
Compiled from "Math.java"
public class Math {
public static int initDate;
public static User user;
static {};
Code:
0: sipush 666
3: putstatic #12 // Field initDate:I
6: new #14 // class User
9: dup
10: invokespecial #16 // Method User."<init>":()V
13: putstatic #19 // Field user:LUser;
16: return
public Math();
Code:
0: aload_0
1: invokespecial #23 // Method java/lang/Object."<init>":()V
4: return
----------------------分割线----------------------
public int computes();
Code:
//0: 将int类型常量1压入操作数栈的栈顶
0: iconst_1
//1: 先将操作数栈的栈顶的int类型值弹出栈顶,然后存入局部变量表的1号索引位上的本地变量(由于computes()是非静态的方法,第0位索引位存放了当前对象的引用,所以是从1号索引位开始分配本地变量的)
1: istore_1
2: iconst_2
3: istore_2
//4: 将局部变量表中1号索引位的int类型本地变量的值压入操作数栈的栈顶
4: iload_1
5: iload_2
//6: 先让栈顶依次弹出两个元素,将弹出栈顶的两个int型数值相加并将结果压入操作数栈的栈顶
6: iadd
//7: 将单字节的int类型常量(-128~127)10压入操作数栈的栈顶
7: bipush 10
//9: 先让栈顶依次弹出两个数值,将弹出栈顶的两个int型数值相乘并将结果压入操作数栈的栈顶
9: imul
10: istore_3
11: iload_3
12: iload_2
//13: 先让栈顶依次弹出两个数值,将弹出栈顶的两个int型数值相除并将结果压入操作数栈的栈顶
13: idiv
//14: 先将操作数栈的栈顶的int类型值弹出栈顶,并存入局部变量表的4号索引位上的本地变量(由于之前局部变量表中存放本地变量的索引位已到3,故采用带参的istore指令)
14: istore 4
//16: 将局部变量表中4号位的int类型本地变量的值压入操作数栈的栈顶(由于之前局部变量表中存放本地变量的索引位已到3,故采用带参的iload指令)
16: iload 4
//18: 从当前方法返回此int类型值(从computes()返回到main()中),具体是将返回值15从conmeputes()方法栈帧里操作数栈的栈顶弹出,然后压入main()方法栈帧里操作数栈的栈顶。而后通过方法出口(即返回地址,存放mian()方法中当前指令的下一步指令的地址)找到main()方法当前指令的下一步指令,以执行main()方法当前指令的后续指令。最后将整个栈帧弹出线程栈。
18: ireturn
public static void main(java.lang.String[]);
Code:
//0: 在堆上为Math对象分配内存空间,并将对象内存地址指针(引用值)压入操作数栈的栈顶
0: new #1 // class Math
//3: 复制操作数栈栈顶的数值(数值不能是long或double类型的)并将其复制后又压入操作数栈的栈顶
3: dup
//4: 从操作数栈栈顶弹出一个对象引用的值,然后通过这个引用值找到并调用此对象的实例初始化方法(实例方法可理解为可以通过具体对象的引用去调用的方法。如对象引用变量.方法)
4: invokespecial #34 // Method "<init>":()V
//7: 先将操作数栈的栈顶的引用类型的值弹出栈顶,而后将操作数栈栈顶的引用型数值存入局部变量表中1号索引位的本地变量math(由于main方法里有个形式参数String[] args,它先于mian()方法内局部变量,在0索引位先存放了args这个数组型本地变量的值,所以后续的索引位从1开始)
7: astore_1
//8: 将1号索引位的引用类型本地变量的值压入操作数栈的栈顶,这里压入的值其实就是this的值(指针)
8: aload_1
//9: 从操作数栈栈顶弹出一个引用的值(即弹出this的值),然后通过这个引用值找到并调用computes()实例方法。invokevirtual是一种动态分派的方法操作指令:引用变量的类型并不能决定到底调用哪个类型的方法,而是根据具体对象属于哪个类来调用该类的这个方法。底层上看,这个指令是导致动态绑定(多态)的原因之一。
9: invokevirtual #35 // Method computes:()
//12: 先将操作数栈的栈顶的int类型值弹出栈顶,然后存入局部变量表的2号索引位上的本地变量
12: istore_2
//13: 获取指定类的静态域(这里是获取PrintStream对象在堆中的地址指针),并将其值压入操作数栈的栈顶
13: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
//16: 将局部变量表中2号索引位的int类型本地变量的值(15)压入操作数栈的栈顶
16: iload_2
//17: 从操作数栈的栈顶以此将弹出两个元素(先弹15,后弹PrintStream对象在堆中的地址指针),通过对象地址指针找到PrintStream.println()并将15这个值传入方法里,调用PrintStream.println()方法(结果在控制台打印15)
17: invokevirtual #43 // Method java/io/PrintStream.println:(I)V
//20: 从当前方法返回void。main()方法执行完毕
20: return
----------------------分割线----------------------
}
上面代码中我已对分割线内的几乎所有字节码指令的功能作出解释。下面我准备画图去简要分析 Run-Time Data Areas 的栈结构中的数据是如何流转的,我将会在某些我认为重要的程序运行节点去作图。这些图所能表示的流程只限于分割线内的指令码。
main() code 0 这行指令码运行完时:
main() code 3 这行指令码运行完时:
main() code 4 这行指令码运行完时:
main() code 7 这行指令码运行完时:
main() code 8 这行指令码运行完时:
到此之后,下一步指令是调用 computes() 方法。
main() code 9 时,computes() code 0 后:
main() code 9 时,computes() code 1 后:
main() code 9 时,computes() code 5 后:
main() code 9 时,computes() code 6 后:
main() code 9 时,computes() code 9 后:
main() code 9 时,computes() code 13 后:
main() code 9 时,computes() code 16 后:
main() code 9 时,computes() code 18 后:
到此,computes() 方法执行结束,computes() 栈帧弹出线程栈。而后该继续执行 main() 方法内接下来的语句了。
main() code 12 这行指令码运行完时:
main() code 20 这行指令码运行完时:
到此,main() 方法执行结束,main() 栈帧从线程栈中弹出,整个线程栈中一切的一切皆归于平静_。
搞这么麻烦到底为什么?其实这是一种学习代码,分析代码的方法。有时候很多取巧的东西不如这种笨办法:一步一步来,边分析边画图,边画图边分析,用 JVM 字节码指令对应其操作的区域去理解 Run-Time Data Areas 结构将会令人印象更加深刻!
目前只对线程栈进行了简单地分析,其实 Run-Time Data Areas 中的各个结构是相互联系的,一发动而牵全身。在 JVM 运行时,不仅栈存在数据流转,其余结构中也同样存在数据流转,其细节还是很复杂的。对此要始终保持敬畏之心。