栈的概念
程序执行的基本原理:CPU有一个指令指示器,指向下一条要执行的指令,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。程序从main函数开始顺序执行,函数调用可以看作一个无条件跳转,跳转到对应函数的指令处开始执行,碰到return语句或者函数结尾的时候,再执行一次无条件跳转,跳转回调用方,执行调用函数后的下一条指令。
函数调用过程中,函数调用方和函数自己就如何存放和使用这些数据达成一个一致的协议或约定。这个约定在各种计算机系统中都是类似的,存放这些数据的内存有一个相同的名字,叫栈。
栈是一块内存,但它的使用有特别的约定,一般是先进后出,往栈里添加数据称为入栈,最下面的栈称为栈底,最上面的栈称为栈顶,从栈顶拿出数据通常称为出栈。栈一般是从高位地址向低位地址扩展,栈底的内存地址是最高的,栈顶的是最低的。
计算机系统主要用栈来存放函数调用过程中需要的数据,包括参数,返回地址,以及函数内定义的局部变量。返回值不太一样,它可能放在栈中,但它使用的栈和局部变量不完全一样,有的系统使用CPU内的一个存储器来存储返回值,我们可以简单的认为存在一个专门的返回值存储器。main函数的相关数据放在栈的最下面,每调用一次函数,都会将相关函数的数据入栈,调用结束会出栈。
函数执行的基本原理
1 public class Sum {
2
3 public static int sum(int a, int b) {
4 int c = a + b;
5 return c;
6 }
7
8 public static void main(String[] args) {
9 int d = Sum.sum(1, 2);
10 System.out.println(d);
11 }
12 }
上述例子中,我们在main函数中调用了Sum类内的函数sum,计算1和2的和然后输出结果,从栈的角度理解下:
当程序在main函数调用Sum.sum(1,2)之前,栈的情况如下:
栈中主要存放了两个变量args和d,在程序执行到Sum.sum()函数内部准备返回之前,即第5行,栈的情况如下:
在main函数调用Sum.sum函数时,首先将参数1和2入栈,然后将返回地址(也就是调用函数结束后要执行的指令地址)入栈,接着跳转到sum函数,在sum函数内部,需要为局部变量 c分配一个空间,而参数变量a和b则直接对应于入栈的数据1和2,在返回之前,返回值保存到了专门的返回值存储器中。
在调用return后,程序会跳转到栈中保存的返回地址,即main的下一条指令地址,而sum函数相关的数据会出栈。main的下一条指令是根据函数返回值给变量d赋值,返回值从专门的返回值存储器中获得。
从上述例子中我们可以看出,函数中的参数和函数内定义的变量都分配在栈中,这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。这个说法主要针对基本的数据类型。
数组和对象的内存分配
对于数组和对象类型,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际内容的空间不是分配在栈上的,而是分配在堆中,但存放地址的空间是分配在栈上的。
public class ArrayMax {
public static int max(int min, int[] arr) {
int max = min;
for(int a : arr){
if(a>max){
max = a;
}
}
return max;
}
public static void main(String[] args) {
int[] arr = new int[]{2,3,4};
int ret = max(0, arr);
System.out.println(ret);
}
}
main函数新建了一个数组,然后调用函数max计算0和数组中元素的最大值,在程序执行到max函数的return语句之前,内存中栈和堆的情况如下:
首先main函数内,栈中主要存放了三个变量,参数args,定义的数组arr,变量ret,栈中存放数组变量arr的内容地址,实际的数组内容存放在堆中。调用函数max时传入参数0和arr,0和arr入栈,然后将函数调用完成后要执行的指令地址入栈,接着进入函数max内部,变量max入栈,运算完成后将结果存储在返回值存储器中。
对于数组arr,在栈中存放的是实际内容的地址0x1000,存放地址的栈空间会随着入栈分配出栈释放,但存放实际内容的堆空间不受影响。但说堆空间完全不受影响是不正确的,在这个例子中,当main函数执行结束,栈中没有变量指向实际内容所在的堆空间的时候,Java系统会自动进行垃圾回收,从而释放这块空间。
递归调用的原理
通过栈的角度来理解递归函数的调用过程。
public static int factorial(int n){
if(n==0){
return 1;
}else{
return n*factorial(n-1);
}
}
public static void main(String[] args) {
int ret = factorial(4);
System.out.println(ret);
}
在factorial函数第一次被调用的时候,n为4,在执行到nfactorial(n-1)即4factorial(3)之前,栈的情况如下:
返回值存储器是没有值的,在调用factorial(3)后,栈的情况如下:
栈的深度增加了,返回值存储器依旧是空,就这样,每递归调用一次,栈的深度就增加一次,每次调用都会分配对应的参数和局部变量,也都会保存调用的返回地址,在调用到n=0的时候,栈的情况如下:
这时,有返回值了。f(0)的返回值为1,f(0)返回到f(1),f(1)的返回值是11,也为1,f(1)的返回值返回到f(2)中,f(2)的返回值为:21=2,f(2)的返回值在返回到f(3)中,f(3)的返回值为:32=6,f(3)的返回值返回到f(4)中,f(4)的返回值为:46=24,f(4)的返回值返回到main函数中,赋值给main函数中的局部变量ret,然后接着执行下一指令。
以上就是递归函数的执行过程,执行过程中,每调用一次,就会有一次入栈,生成一份不同的参数,局部变量和返回地址。