这里写自定义目录标题
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版)