JAVA执行引擎工作原理:方法调用的原理一

本文深入探讨了函数调用的原理,从物理机器的汇编语言层面,详细阐述了方法调用、取指和指令执行的过程。接着,介绍了C语言的函数调用机制,它依赖于物理机器的call指令。最后,讨论了JVM如何通过C语言实现对Java函数的调用,揭示了JVM内部的callstub函数指针在调用边界上的作用。通过这些,我们理解了从硬件到软件层面,函数调用的完整流程。
摘要由CSDN通过智能技术生成


JVM作为一款虚拟机,也必然要涉及计算机核心的3大功能。
1.方法调用
方法作为程序组成的基本单元,作为原子指令的初步封装,计算机必须能够支持方法的调用。同样, Java语言的原子指令是字节码, Java方法是对字节码的封装,因此JVM必须支持对Java方法的调用。
2.取指
这里的“取指”,是指取出指令。还是那句话,方法是对原子指令的封装,计算机进入方法后,最终需要逐条取出这些指令并逐条执行。 Java方法也不例外,因此JVM进入Java方法后,也要能够模拟硬件CPU,能够从Java方法中逐条取出字节码指令。
3.计算机取出指令后,就要根据指令进行相应的逻辑运算,实现指令的功能。JVM作为虚拟机,也需要具备对Java字节码的运算能力。
JAVA是一门可以跨平台的语言,在运行时将Java字节码指令动态翻译成本地机器指令,从而既能获取兼容性,又能获取很高的运行效率。

方法调用

真实的机器调用

本节主要是通过汇编程序讲解一些真实的机器调用原理。真实机器指令调用机制涉及较多,比如现场保护、堆栈分配、参数传递等等。
下面这段程序是使用汇编编写的,实现对两个整数进行求和的功能。
在这里插入图片描述
在这里插入图片描述
一般地,函数调用有调用方和被调用方,操作系统作为main函数的调用者,是一个程序的最开始的调用者,也是程序的入口。在这个程序中有这样的关系:os调用了main函数,main函数在执行过程调用了add方法。那么,程序是如何来保证程序能够有条不紊地执行下去?计算机是使用了EIP寄存器(PC)来保存CPU下次要执行的指令的地址,这主要是为了让main方法执行完call调用回来之后,能够继续处理main方法接下来的指令。具体操作:物理机器执行call指令时,自动将当前eip指令入栈。当执行完调用后,物理机器再将eip出栈,这样,执行完调用函数之后,物理机器会截止执行调用者的指令。
下面贴一下上面程序调用过程的架构图:
在这里插入图片描述

由图可以知道:

看过main)函数的代码注释后,我们知道,main()函数一共包含5步:保存调用者栈基地址,·初始化数据,压栈,函数调用和返回。下面分别分析以下过程。
1)保存栈基并分配新栈首先看第一步, main()函数从下面两条指令开始执行:
保存调用者的栈基地址,方便调用者(操作系统)找到被调用函数(main函数),并获取返回值并继续执行调用者的逻辑,通过动态地计算,给main函数分配了32字节的新栈,pushl %ebp就是保存调用者的栈基地址,movl %esp, %ebp将调用者的栈基地址指向其栈顶。这两句是所有函数调用时都必定执行的指令。add函数也是同样的道理,只不过此时的调用方式main函数,不是操作系统。
在这里插入图片描述
在这里插入图片描述
2)初始化数据:
即初始化局部变量
在这里插入图片描述
3)压栈
方法通过形参列表来获取实参,但是实参并不是直接从局部变量表获取,每个方法的栈帧的栈顶存放着参数列表。add函数按照参数依次从main函数的栈顶获取,并加载到add的局部变量表中。
main:main函数将局部变量表中的变量压栈到栈顶中。
在这里插入图片描述
add:从main函数栈顶获取参数到add局部变量表中
在这里插入图片描述
4)函数调用
在这里插入图片描述

3.获取参数
参数的获取是main函数将实参压栈到栈顶的位置,add函数根据形参的多少一次从main的栈顶获取。

在这里插入图片描述

4.返回返回值,回收栈空间
在这里插入图片描述
通过对这段汇编程序的分析可知,物理机器在执行程序时,将程序划分成若干函数,每个函数都对应有一段机器码。一段程序的机器码都放在一块连续的内存中,这块内存叫做代码段。物理机器为每一个函数分配一个方法栈,方法栈与代码段在地址上没有任何关系,并且只有当物理机器执行到某个函数时,才会为其分配方法栈,否则就不会分配。函数通过自身的机器指令遥控其对应的方法栈,可以往里面放入数值,也可以将数值移动到其他地方,也可以从里面读取数据,也可以从调用者的方法栈里取值。通过一条条指令和一个个栈,物理机器得以运行完一整个程序。

C语言函数调用

讲完物理机器函数调用的原理后,接下来我们一起看看C语言是如何实现函数调用的。毕竟JVM是混合C和C++开发而成的, JVM执行引擎最关键的一点就在于实现了由C语言动态执行机器指令,这正是JVM与机器指令的边界所在。
C语言属于静态编译型语言, C语言开发的程序被编译后,直接生成二进制代码,而这些,二进制代码正是由一条条机器指令组成,因此可直接被物理机器执行。所以, c程序中的函数调用,本质上还是依靠物理机器所提供的call指令来完成。

int add();
int main(){
	int c=add();
	return 0;
}
//无参函数
int add(){
	int z=1+2;
	return z;
}

将C语言编译成汇编程序。由程序可以知道,这里调用的是无参函数,main的栈帧中的栈顶参数表没有参数,所以不需要压栈操作。
在这里插入图片描述

int add(int a,int b);
int main(){
    int a=5;
    int b=3;
	int c=add(a,b);
	return 0;
}
//有参函数
int add(int a,int b){
	int z=1+2;
	return z;
}

这里main函数调用的是有参函数,自然涉及到参数获取操作以及压栈操作。
在这里插入图片描述
在这里插入图片描述

JVM的函数调用机制

JVM是用C和C语言编写的一款软件,当JVM执行Java函数时,实际上是执行了一段汇编代码,换言之,这中间一定存在一个边界,在边界处,边界的一边是C程序,边界的另一边直接是机器指令, C语言要能够直接执行机器指令。例如,如果你编写了下面一段非常简单的Java代码:

public class Test {
    public static void main(String[] args) {
        add(5,8);
    }
    public static int add(int a,int b){
        int c=a+b;
        int d=c+9;
        return d;
    }
}

在这里插入图片描述
在这里插入图片描述
Java字节码指令多对应的机器指令逻辑:
在这里插入图片描述
虽然JVM执行的是字节码指令,但实际执行的是对应的一大串机器码。
在JVM内部也有这么一个函数指针,就是call stub。这个函数指针正是JVM内部C语言程序与机器指令的分水岭, JVM在调用这个函数之前,主要执行C程序(其实还是C程序编译后的机器指令),而JVM通过这个函数指针调用目标函数之后,就直接进入了机器指令的领域。

参考书籍:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cai_beyond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值