一:编程语言兼容底层系统的方式大概分为两种
1、通过编译器实现兼容
例如C、C++等编程语言,既能运行与Linux系统,也能运行与Windows系统;既能运行于x86平台,也能运行于AMD平台。这种能力并不是编程语言所具备的,而是由编译器所赋予的。针对不同的硬件平台和操作系统,开发特定的编译器,编译器能够将同一段C/C++语言翻译成与目标平台匹配的机器指令,从而实现语言的兼容性。
2、通过中间语言实现兼容
Java、C#等语言,都是属于这种兼容方式
Java/C#被编译后,生成中间语言(ML),中间语言指令由虚拟机负责解释和执行。虚拟机在运行期间将中间语言实时翻译成与特定底层平台匹配的机器指令并运行。无论程序最终运行在那种底层平台上,源代码被编译生成的中间语言指令都是相同的。中间语言的兼容性由虚拟机啊负责完成。
二:中间语言翻译
将中间语言翻译成对应的机器指令并得以执行。
1、从中间语言翻译到机器码
一种可行的办法是:使用C程序,将字节码的每一条指令,都逐行的解释成C程序。当执行字节码的程序——JVM程序本身被编译后,字节码指令所对应的C程序呗一起编译成机器码,于是虚拟机在解释字节码指令时,自然会执行对应的C程序对应的本地机器码。
但是这种方式效率太低。
2、直接翻译为机器码
利用CPU执行代码的原理。要让CPU执行一段代码,只需将CS:IP段寄存器执行到代码段入口即可。
CS寄存器保存段地址,IP保存偏移地址。CS和IP两个寄存器的值能够唯一确定内存中的一个地址,CPU在执行指令之前,便通过这两个寄存器定位到目标内存位置,并将该位置处的机器指令取出来进行运算。
例程:利用C程序提供的语法糖(语法规则),让CS:IP直接指向一串机器码
const unsigned char code[] = "\x55\x89\x35\x8b\x45\x0c\x8b\x55\x08\x01\xd0\x5d\xc3";
int main(){
int a = 5;
int b = 3;
int (*fun)(int,int);//定义函数指针
fun = (void*)code;//初始化函数指针,将其指向code机器码的入口
int r = fun(a,b);
printf(r);
return 0;
}
本例实现两个正整数之和。fun指针指向一个char数组首地址。
3、本地编译
虽然将中间语言翻译为机器码并直接运行,其效率比使用C语言来解释执行,已经提高了很多,但是,由于中间语言有自己的一套内存管理和代码执行方式。因此,实现同样的功能,虽然使用中间语言只需写几行代码,但是翻译后的机器码,比直接编写机器码,还要多出很多指令。效率不高。
为提供性能,JVM提供了一种机制,能够将中间语言(字节码)直接翻译为本机器指令。
三:指令
通过编译器将Java语言翻译成中间语言,然后再交给虚拟机,其再将中间语言翻译成对应机器平台上的指令。
所谓的中间语言就是Java字节码指令集。
Java的所有的指令都是用8位二进制描述,因此Java的指令总数不超过255个。
1、常见的汇编指令
主要学习java虚拟机执行引擎的内部实现机制,学习简单的5个汇编指令。
(1)数据传送指令
这些指令主要在寄存器与内存、寄存器与输入/输出端口之间传送数据。例如
//将自然数1传送到eax寄存器
movl 1, %eax
//将站定数据弹出至eax寄存器
pop %eax
(2)算术运算指令
包括基本的四则运算、浮点运算、数学运算(正弦等)
//将自然数3与eax寄存器中的数累加,并将结果存进eax中
add 3 ,%eax
// 对ebx寄存器中的数加增1
inc %ebx
(3)逻辑运算符
与、或、非、左移、右移等指令
//将eax寄存器中的数左移1个二进位
shl %eax,1
//对al寄存器中的数和操作数进行与操作
and al ,00111011B
(4)串指令
连续空间分配,连续空间取值,传送等。
(5)程序转移指令
if else判断、for循环、while循环、函数调用等
常见的:jmp跳转、loop循环、ret等
四:JVM指令
Java是面向对象的编程语言,自然要有一套支持类型操作的特殊指令。
1、数据交换指令
对JVM内存而言,分为操作数栈、局部变量表、java堆、常量池、方法区。数据交换指令就是支持数据在这些内存区域之间传送和交换。JVM执行逻辑运算的主要地方是操作数栈(iinc指令除外,该指令可以直接对局部变量进行运算)。无论你把数据放在堆栈中,还是放在常量池,只要执行运算,最终JVM都会将数据传送到操作数栈中。而硬件执行运算的区域是寄存器,无论你把数据放在数据段中,还是代码段,最终CPU都会讲数据传送到寄存器中。逻辑运算完成后,再把结果转义出去。
JVM规范还提供了像getfeild和putfeild这样的指令:实现java堆中的对象的字段和操作数栈之间的数据交换;
getstatic和putstatic这样的指令:实现类中的字段和操作数栈之间的数据交换;
baload、bastore、caload和castore这样的指令:实现JVM堆中的数组和操作数栈之间的数据交换
2、函数调用指令
由于Java中的函数类型比较丰富,因此必然要支持更多的函数调用方式。例如:invokevitual、invokeinterface、invokespecial、invokestatic和return等。这比硬件所支持的函数调用指令集要丰富一些。x86主要使用call和ret来保存现场和恢复现场,往往会伴随CPU物理寄存器入栈和出栈。
JVM没有物理寄存器,所以用操作数栈和PC寄存器来替代。JVM保存现场和恢复现场的解决方案是向Java堆栈中压入一个栈帧,函数返回时从java堆栈中弹出一个栈帧。
JVM调用函数时候,不能像CPU硬件那样,直接跳转就能找到对应代码段。这是因为Java函数的代码并没有被存放在代码段中,而是放在了一个code缓存中。每一个Java函数代码块在这个code缓存中都会有一个索引位置,最终JVM会跳转到这个索引位置处执行Java函数调用。同时,Java的函数一定是被封装到类中的,因此JVM在执行函数调用时,还需要经过寻址等等一些列运算,最终才能定位这个入口。
3、运算指令集
JVM和运算相关的指令集主要有算术运算,位运算,比较运算,逻辑运算等。
常见运算指令: iadd:对两个int型数据求和 isub:对两个int型整数做减法 fadd:对两个float浮点数进行求和 ddiv:两个double双精度型数据相除等
4、控制转移指令
与CPU硬件一样,JVM也提供了常见的控制转移指令。
5、对象创建一类型转换指令
JVM提供一套创建对象的指令。在Java语法层面使用关键字new可以实例化一个对象,而对应的字节码也是new。
JVM规范还提供了“窄化类型转换”指令与“宽化类型转化”指令。后者JVM内部天生支持。
除了上述指令外,JVM规范还提供很多其他物理CPU没有的指令。例如,抛出异常指令,用于线程同步的指令等。
总结:
Java语言所要解决的是如何能够不关注底层技术细节就能实现兼容性,通过中间语言来实现跨平台兼容的目标。由于中间语言并不是本地机器指令,机器无法直接识别,因此中间语言并不能直接由物理CPU运行。使用虚拟机来解释中间语言,将中间语言翻译成与之对应的本地机器语言。
高效的方式是直接将Java字节码指令翻译成本地机器指令,运行期直接由Java虚拟机调用对应的机器指令来执行,这种调用机制主要依靠CPU所提供的call和jmp指令。
ps:
该学习笔记基于学习封亚飞作者的《揭秘Java虚拟机—JVM的设计原理与实现》。第一次写博客并发布,基本上都是学习笔记,如有侵犯版权请联系我删除。喜欢就点赞吧。