刚开始尝试深入写JVM相关内容,语言尽量通俗,有不懂的地方欢迎留言一起探讨~
写在最前面
James Gosling,java创始人,被称之为“java之父”,从write once, run anywhere!
可以看出,James其实是想开发一款可以在任何平台运行的语言。在当时,其实很多编程语言都具备了这种能力,比如c语言,估计最难的一点就是怎么样在开发层面实现平台的无关性了。
那么c语言又是怎么实现兼容的呢?
c语言在实现兼容的方式可谓是简单粗暴啊,不同的平台就用不同的编译器嘛,直接将c语言编译成底层平台可以运行的机器指令。虽然这种简单粗暴的方式还算是很好的解决了兼容性问题,但是开发者就惨了,不同的系统底层调用的API是不一样的,开发者在做代码开发的时候不光要关注功能相关逻辑,还必须要关注底层的API。
James可不想这样累死开发者,对于这个问题,他想了一个解决办法,我们为何不搞一个专门的模块帮开发者做这些呢,就这样,虚拟机(JVM)和字节码规范就应运而生了,程序会被编译成字节码,由虚拟机解释执行字节码。
注:其实综上所述,一款语言要做底层系统的兼容性大致分为两种方案:
通过编译器实现兼容:
比如c/c++,编译器赋予了它们可以在不同平台运行的能力。针对不同的系统,开发特定的编译器,编译器可以把程序翻译成平台可以识别的机器指令,从而实现兼容性。通过中间语言实现兼容:
比如java,编译后,生成中间语言,虚拟机解释运行该中间指令,无论程序最重运行在哪个平台,编译生成的中间语言指令都是相同的(.class文件),至于和平台的兼容性,由虚拟机来完成。
划水半篇文章,顺便介绍下java设计的一些背景知识后,接下来我们就进入正题,看看JVM底层到底是怎么实现方法调用的。
方法调用
为什么要先介绍方法调用?
其实我也不想先介绍方法调用,但是它是基础啊,是整个Java执行引擎可以正常run起来的重点。说白了,JVM作为一款虚拟机,它肯定是需要涉及到计算机的3大核心功能的:
方法调用:学过计算机的都知道,方法是作为程序组成的一个最基本的单元,而对于Java来说,原子指令其实就是字节码,Java方法也就是对字节码的封装,Java程序要想愉快的run起来,那JVM必须要支持对Java方法的调用;
取出指令:方法是对原子指令的封装,那最终在CPU上执行其实也就是指令逐条取出并执行,Java的方法执行也是一样的流程,这个时候就需要JVM配合了,JVM需要模拟CPU,逐条取出字节码指令并执行;
运算:CPU取出指令就可以根据指令做相应的逻辑运算了,当然,JVM也需要具备字节码的运算能力。
提起方法调用,有了解过JVM的一定多少有听说过call_stub()
函数。对,就是它,该函数在整个JVM中有着非常重要的作用,接下来我们就来看看JVM是如何实现的。
call_stub函数定义
CallStub函数定义从源码可以看出,call_stub函数调用了一个宏
CAST_TO_FN_PTR
,我们来看下这个宏干了些什么:
#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))
把call_stub函数的宏替换下:
CallStub函数替换宏定义
函数定义已经清晰了,那我们来看看CallStub这个JVM自定义的类型:
CallStub定义
看到这里小伙伴们肯定不淡定了,这个call_stub函数不就是c语言里典型的函数指针么。指向的函数返回值类型是void,并且有8个入参,接下来我们就以call_stub函数的调用来依次做相关介绍。
注:c语言中相近的定义还有指针函数,但是它俩是完全不同的,一个重点是函数,一个重点是指针变量,具体的差别有兴趣的小伙伴请自行百度~~~
call_stub函数调用
JVM在javaCalls::call_helper()
调用了该函数,我们来看看是怎么调用的:
CallStub调用注:javaCalls::call_helper()在javaCalls.cpp中实现
从源码可以看出,JVM隐式的调用了函数指针,我们来改一下这段源码,就一目了然了:
修改后的CallStub调用
由于JVM在申明CallStub的时候就定义了该函数指针需要8个入参,所以JVM最终在调用的时候也按照约定,传入了8个类型相同的参数。我们再结合call_stub()方法的定义来具体还原call_stub()方法的逻辑。
call_stub函数逻辑还原
上文已经简单给出了call_stub函数的定义,从call_stub函数定义可以知道,它其实是调用了方法castable_address方法,并将其转换成CallStub类型(函数指针),JVM通过调用其函数指针完成函数的调用,说白了,call_stub的目的就为了让函数指针指向某个函数(内存地址)~
castable_address实现
castable_address实现注:castable_address方法定义在globalDefinitions.hpp中
从源码可以看出,castable_address方法将入参x转换成了address_word类型:
address_word定义注:address_word也是一个自定义类型,同样定义在globalDefinitions.hpp中
从address_word定义可以看出,它的类型其实是uintptr_t,从源码注释可以看出来,uintptr_t其实是一个unsigned integer(无符号整数),由于这种类型跟平台相关,所以JVM在3个地方定义了改类型:
-
globalDefinitions_gcc.hpp:Linux操作系统
-
globalDefinitions_sparcWorks.hpp:MacOs操作系统
-
globalDefinitions_visCPP.hpp:Windows操作系统
我们以Linux平台为例,我们来看一下uintptr_t的定义:
uintptr_t定义
call_stub基本逻辑还原
到这里,call_stub函数可以继续替换成这样:
call_stub替换实现
从替换后的源码实现可以看出:
-
call_stub函数首先将
_call_stub_entry
转换成unsigned int
类型; -
将转换后的
unsigned int
类型转换成CallStub
类型。
不是说好的call_stub()会让CallStub函数指针指向某个函数么,怎么指向的啊?就只看到了把_call_stub_entry
转换成了CallStub类型~~~对,你想的没错,_call_stub_entry
就是待调用函数的内存地址,我们来看看_call_stub_entry
相关声明和初始化吧。
_call_stub_entry
- _call_stub_entry声明:
_call_stub_entry声明注:_call_stub_entry声明在subRoutines.hpp中
- _call_stub_entry初始化:
在JVM初始化的时候,_call_stub_entry就会被初始化指向某一个内存地址,以Linux x86 64位系统为例:
_call_stub_entry初始化注:_call_stub_entry初始化位于stubGenerator_x86_64.cpp中
从加框部分源码可以看出来,_call_stub_entry是通过方法
generate_call_stub
完成初始化的。
注:
generate_call_stub
方法中涉及到堆栈内存分配等操作,是JVM核心功能,下一篇文章会单独做详细分析介绍,在这里就不做深入介绍~
CallStub入参
在开始下一篇文章详细分析generate_call_stub()方法初始化_call_stub_entry之前,再做最后一个入门的基础知识介绍:CallStub的8个入参。
我们来回想上文中给出的JVM调用call_stub()方法源码,JVM在调用时一共传入了8个参数:
-
link
JavaCallWrapper定义
连接器,类型是JavaCallWrapper,我们来看一下该类型的定义,看看这个link到底想要链接谁~
从源码可以看出,JavaCallWrapper主要包含以下私有变量:-
_thread:当前函数所在线程;
-
_handles:调用句柄;
-
_callee_method:调用者方法对象;
-
_receiver:被调用者;
-
_anchor:Java线程堆栈对象;
-
_result:方法返回值。
通过这些私有变量可以看出,link主要连接了函数的调用者和被调用者~当然,函数在调用时,link指针也会被保存到当前方法的堆栈中。
-
-
result_val_address
函数返回值地址。 -
result_type
函数返回类型。 -
method()
当前方法在JVM中的表示对象。每一个方法在被加载的时候,JVM都会为其建一个模型,保存该方法所有的原始描述信息,主要包括:-
方法的名称,所属的类;
-
方法的入参信息,包括入参类型,入参参数名,入参数量,顺序等;
-
方法编译后的字节码信息,包括对应的字节码指令等;
-
方法的注释信息;
-
方法的继承信息;
-
方法的返回信息
method()参数的意义就是为了让JVM可以通过method()对象获取到Java方法编译后的字节码信息,JVM在拿到字节码后就可以解释执行了~
-
-
entry_point
JVM每次在调用Java函数时,必然会调用CallStub函数指针,当然咯,这个函数指针的值就是_call_stub_entry,JVM通过_call_stub_entry指向被调用函数地址,最终调用函数。在调用函数之前,必须要先经过entry_point,JVM实际是通过entry_point从method()对象上拿到Java方法对应的第一个字节码命令,这也是整个函数的调用入口。 -
parameters()
方法入参信息。JVM在调用函数之前,会通过该参数为函数分配堆栈,并将入参入栈。 -
size_of_parameters()
方法入参数量。 -
CHECK
当前线程对象。
到这里为止,函数调用一些入门的基础就介绍完了。下一章继续啃骨头,_call_stub_entry的初始化~
网友评论