上一节课,我们讲了函数栈与栈帧,以及形式参数和实参的区别。这一节课,我们再细化一点,看一下Java的栈帧里的具体内容。
昨天的课程里,有同学私信我,问Java栈帧和操作数栈是什么关系。可能是我的图画得比较有误导性,所以我今天把图重新组织了一下。
Java虚拟机在执行字节码的时候,会使用一个叫做操作数栈的数据结构来进行运算,还会在函数栈帧上保存一个叫做局部变量的结构,用来存储各个局部变量的值。
还是拿上节课的例子来讲:
public
那么函数栈帧是怎么组织的呢?结合上节课的内容,虚拟机会开辟一段内存做为栈帧。栈帧上有局部变量表,还有操作数栈。我们以暗蓝色代表局部变量表,那么,函数对应的帧大体上是这个样子的:
一开始,a, b, t 都只是占了一个位置,而没有具体的值。我们来看字节码,一行行地分析JVM是如何执行字节码的。还是老规矩,把字节码打印出来:
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
第0条指令,iconst_1,代表把常量1,送到操作数栈顶,那么现在操作数栈就变成了:
第1条指令,istore_1,代表取出栈顶的值,并将其存入到局部变量表的第一个位置,也就是变量a对应的位置。这时,栈帧的内容变成了这样:
iconst_2和istore_2与上面的过程是一样的,我就不再重复画了。执行完这两行之后,栈的内容变成了:
接下来,是第4行和第5行,iload_1就是把局部变量表的第一个位置,也就是a的值1,送入栈顶,iload_2是把局部变量表的第二个位置,就是b的值2,送入栈顶。这时栈,就变成了这个样子:
这里有同学可能会有疑问,1和2的位置是不是画反了。还真不是,在x86或者x64这个体系结构上,栈是从高地址向低地址增长的,所以栈底在上面,栈顶在下面。
接下来,又是iadd了。这条指令,我们已经连续三节课分析它了,是老朋友了。相信大家都能记住了,iadd三件事:从栈里弹出两个数,求和,把和送到栈顶。所以,经过了iadd指令以后,栈帧就变成这样了:
最后再执行istore_3,把栈顶上的3送到局部变量表的第3个位置。就是t,这个过程与前面的istore_1, istore_2是相似的。所以不再画图了。
栈和堆
下面继续今天的重点。我们上节课,介绍了在函数调用的时候,往新的栈帧传递参数的时候,会把参数从调用者的栈帧里往被调用者的栈帧里复制一份。在被调用者的栈帧里对参数进行修改,只能修改被调用者的那份副本,而调用者栈里的那一份数据是改变不了的。但是,必须要加一个条件,那就是参数都是基础数据类型,比如int, float, double等等。对于普通的Java对象就不成立了。
大家可以先想一下,假设参数是Java对象,JVM在调用函数的时候,把对象拷贝一份进入到谳用者的栈帧里去,如果对象很大,比如有几十上百个成员变量,那这个消耗就很可观了。有时,返回值还可能是一个大的对象。怎么解决这个问题呢?其实从C语言的时代,针对这个问题,就有了解决方案,那就是堆和指针。Java继承了这一方案,堆的概念在Java仍然适用,只是指针变成了引用(reference)。
堆(heap)是一块独立的内存空间。在创建对象的时候,我们可以把真正的对象放到堆里,只在栈里记录这个对象的地址就可以了。我们可以凭借这个地址找到真正的对象,所以,引用这个名称还是挺形象的。先看代码:
public
对照昨天的作业,我们看到,如果swap函数的参数类型是整型的时候,并不能交换两个数的值,而如果参数类型是普通的Java class,则可以交换这两个对象的value值。好了,上图:
可以看到,在main的栈里,和swap的栈里,都只是存了一个指向堆里真实对象的引用,也就是一个地址。如果在swap里,把A1中的value和A2中的value对换,那么,当swap结束调用以后,在main里,看到A1的value和A2的value就已经对换了。
这就是函数调用时传值与传引用的区别。
好了。今天的课程就到这里了。我们虽然还没有把函数的全部机制都讲清楚的。但是掌握这些知识已经足够我们去完成递归下降的函数设计了。明天就会真正地开始使用递归下降去写表达式求值了。
今天的作业:
1. 去网上找一下Java语言规范,文档的英文名:The Java Language Specification Java SE 8 Edition。看看Java里还定义了哪些字节码。尝试着理解一下。
2. 写一个带有 if , while, for 的程序,使用javap -c命令查看它的字节码。看看能不能看懂(看不懂也没关系,我们后面会专门讲,但如果你现在就看懂了,后面会感觉很省力。)
上一节课:深入理解函数调用(上)
下一节课:
目录:课程目录