最近由于论文的原因在做深度学习的一些东西,很长时间没有接触安卓开发和Java的知识,过年又面临找工作,忙里偷闲决定每周花点时间学习学习开发,今天就来复习一下Java函数调用基本原理,参考书籍为《Java编程的逻辑》。
我们知道CPU有一个PC,指向下一条要执行的指令的地址,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。那么程序从main函数开始顺序执行,函数调用可以看作一个无条件跳转,跳转到对应函数的指令处开始执行,碰到return语句或者函数结尾的时候,再执行一次无条件跳转,跳转回调用方,执行调用函数后的下一条指令。这个过程中就会涉及到参数的传递以及函数调用的返回、函数结果的返回等。计算机使用栈(先进后出,栈底的内存地址最高)来存放这些数据,包括参数、返回地址,函数内定义的局部变量等等,而返回值使用的栈和局部变量不完全一样,可以简单认为存在一个专门的返回值存储器。
main函数的相关数据放在栈的最下面,每调用一次函数,都会将相关函数的数据入栈,调用结束会出栈。《Java编程的逻辑》中给出一个例子: main函数调用了sum函数,计算1和2的和,然后输出计算结果
public class Sum {
public static int sum(int a, int b) {
int c = a + b;
return c;
}
public static void main(String[] args) {
int d = Sum.sum(1, 2);
System.out.println(d);
}
}
当程序在main函数调用Sum.sum之前,栈的情况大概是这样的:
存放了两个变量args和d。在程序执行到Sum.sum的函数内部准备返回之前,即return c,栈的情况大概如下图所示:main函数调用Sum.sum时,首先将参数1和2入栈,然后将调用Sum.sum函数结束后要执行的指令地址入栈,接着跳转到sum 函数,在sum函数内部,为局部变量c分配一个空间,而参数变量a和b则直接对应于入栈的数据1和2,并且返回之前返回值保存到了专门的返回值存储器中:
调用return后,程序会跳转到栈中保存的返回地址,即main的下一条指令地址,而sum函数相关的数据会出栈,从而又变回下面这样:
对于上面程序中的基本数据类型来说,函数中的参数和函数内定义的变量,都分配在栈中,这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了。但是数组和对象不同,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际的内容空间一般不分配在栈上而是分配在堆上,存放地址的空间是分配在栈上:
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语句之前的时候,内存中栈和堆的情况大概是这样的:
对于数组arr,在栈中存放的是实际内容的地址0x1000,存放地址的栈空间会随着入栈分配,出栈释放,但存放实际内容的堆空间不受影响。只有当main函数执行结束,栈空间没有变量指向它的时候,Java系统会自动进行垃圾回收(GC),从而释放这块空间。
而对于递归调用来说,例如下面的求阶乘函数:
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,在执行到4*factorial(3)之前的时候,栈的情况大概是:
返回值存储器是没有值的,在调用factorial(3)后,栈的情况变为了:
栈的深度增加了,返回值存储器依然为空,就这样,每递归调用一次,栈的深度就增加一层,每次调用都会分配对应的参数和局部变量,也都会保存调用的返回地址,在调用到n等于0的时候,栈的情况是:
这个时候就有返回值了,将factorial简写为f。f(0)的返回值为1,f(0)返回到f(1),f(1)执行1f(0),结果也是1,然后返回到f(2),f(2)执行2f(1),结果是2,然后接着返回到f(3),f(3)执行3f(2),结果是6,然后返回到f(4),执行 4f(3),结果是24。递归调用函数代码虽然只有一份,但在执行的过程中,每调用一次,就会有一次入栈,生成一份不同的参数、局部变量和返回地址。另外栈的空间不是无限的,一般正常调用都是没有问题的,但如果栈空间过深例如求10000000的阶乘,系统就会抛出错误,java.lang.StackOverflowError,即栈溢出。
最后说一下Java代码的执行步骤:从Java源代码到运行的程序,有编译和链接两个步骤。编译是将源代码文件变成扩展名是.class的一种字节码,这个工作一般是由javac命令完成的。链接是在运行时动态执行的,.class文件不能直接运行,运行的是Java虚拟机,虚拟机执行Java命令解析.class文件,转换为机器能识别的二进制代码,然后运行。所谓链接就是根据引用到的类加载相应的字节码并执行。
参考书籍: