Java函数调用基本原理

最近由于论文的原因在做深度学习的一些东西,很长时间没有接触安卓开发和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文件,转换为机器能识别的二进制代码,然后运行。所谓链接就是根据引用到的类加载相应的字节码并执行。

参考书籍:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值