函数调用方和函数本身就如何传递参数、如何返回结果、如何跳转指令等问题,使用内存来存放这些数据,并且就如何存放和使用这些数据达成一个一致的协议或约定。
这个约定在各种计算机系统中都是类似的,存放这些数据的内存有一个相同的名字,叫栈。
栈是一块内存,顺序是先进后出,类似于一个书堆,越晚放上的书越先拿走。
栈的最下面称为栈底,最上面成为栈顶。
往栈底放数据,称为入栈;往栈顶取数据,成为出栈。
栈一般是从高位地址向低位地址扩展,所以栈底的内存地址是最高的,栈顶的内存地址是最低的。
计算机系统主要使用栈来存放函数调用过程中需要的数据,包括参数、返回地址,函数内定义的局部变量也放在栈中。
1 | public class Sum { |
上面的代码,概念上:main函数调用了sum函数,计算1+2,然后输出计算结果。
从栈的角度:
(1)当程序在main函数调用Sum.sum函数之前,栈的情况如下表格
地址 | 内容 | 函数 |
---|---|---|
0x7FF4 | ||
0x7FF8 | ||
0x7FFC | d | main |
0x8000 | args | main |
栈主要存放了两个变量args(从控制台接收到的参数)和d。
(2)在程序执行到Sum.sum的函数内部,准备return c之前,栈的情况如下表格。
地址 | 内容 | 函数 |
---|---|---|
0x7FEC | 3(c) | sum |
0x7FF0 | 栈中保存的返回地址 | sum |
0x7FF4 | 2(b) | sum |
0x7FF8 | 1(a) | sum |
0x7FFC | d | main |
0x8000 | args | main |
main函数调用Sum.sum时,首先将参数1和2入栈,然后将返回地址入栈,接着跳转到sum函数,在sum函数内部,为局部变量c分配一个空间,而参数变量a和b则直接对应于入栈的数据1和2。在返回之前,返回值3保存到了专门的返回值存储器中。
(3)调用return后,程序会跳转到栈中保存的返回地址,sum函数相关的数据会出战,从而又变回(1)中的状态。
地址 | 内容 | 函数 |
---|---|---|
0x7FF4 | ||
0x7FF8 | ||
0x7FFC | d | main |
0x8000 | args | main |
(4)main的下一条指令是根据函数返回值给变量d赋值,返回值从专门的返回值存储器中获得。
函数中的基本数据类型参数和函数内定义的基本数据类型变量,都分配在栈中。这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。
而数组和对象类型,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址。实际的内容一般不是分配在栈上的,而是分配在堆中,但存放地址的空间是分配在栈上的。
1 | public class ArrayMax { |
上面的代码,概念上:main函数新建一个数组int[]{2,3,4},然后调用函数max计算0和数组中元素的最大值,最后打印输出最大值。
从栈的角度:
栈-地址 | 栈-内容 | 函数 | 堆-地址 | 堆-内容 |
---|---|---|---|---|
0x7FE8 | max(4) | max | 0x1000 | 2 |
0x7EFC | 栈中保存的返回地址 | max | 0x1004 | 3 |
0x7FF0 | 0x1000(arr) | max | 0x1008 | 4 |
0x7FF4 | 0(min) | max | ||
0x7FF8 | ret | main | ||
0x7FFC | 0x1000(arr) | main | ||
0x8000 | args | main |
对于数组arr,在栈中存放的是实际内容的地址0x1000,存放地址的栈空间会随着入栈分配,出栈释放。
存放实际内容的堆空间,在函数结束、栈空间没有变量指向它的时候,java系统会自动进行垃圾回收,从而释放这块空间。
栈的空间不是无限的。栈空间过深,系统就会抛出错误——java.lang.StackOverflowError,即栈溢出错误。