JVM系列之函数调用入门

刚开始尝试深入写JVM相关内容,语言尽量通俗,有不懂的地方欢迎留言一起探讨~

写在最前面

James Gosling,java创始人,被称之为“java之父”,从write once, run anywhere!可以看出,James其实是想开发一款可以在任何平台运行的语言。在当时,其实很多编程语言都具备了这种能力,比如c语言,估计最难的一点就是怎么样在开发层面实现平台的无关性了。

那么c语言又是怎么实现兼容的呢?
c语言在实现兼容的方式可谓是简单粗暴啊,不同的平台就用不同的编译器嘛,直接将c语言编译成底层平台可以运行的机器指令。虽然这种简单粗暴的方式还算是很好的解决了兼容性问题,但是开发者就惨了,不同的系统底层调用的API是不一样的,开发者在做代码开发的时候不光要关注功能相关逻辑,还必须要关注底层的API。

James可不想这样累死开发者,对于这个问题,他想了一个解决办法,我们为何不搞一个专门的模块帮开发者做这些呢,就这样,虚拟机(JVM)和字节码规范就应运而生了,程序会被编译成字节码,由虚拟机解释执行字节码。

注:其实综上所述,一款语言要做底层系统的兼容性大致分为两种方案:

  1. 通过编译器实现兼容:
    比如c/c++,编译器赋予了它们可以在不同平台运行的能力。针对不同的系统,开发特定的编译器,编译器可以把程序翻译成平台可以识别的机器指令,从而实现兼容性。

  2. 通过中间语言实现兼容:
    比如java,编译后,生成中间语言,虚拟机解释运行该中间指令,无论程序最重运行在哪个平台,编译生成的中间语言指令都是相同的(.class文件),至于和平台的兼容性,由虚拟机来完成。

划水半篇文章,顺便介绍下java设计的一些背景知识后,接下来我们就进入正题,看看JVM底层到底是怎么实现方法调用的。

方法调用

为什么要先介绍方法调用?
其实我也不想先介绍方法调用,但是它是基础啊,是整个Java执行引擎可以正常run起来的重点。说白了,JVM作为一款虚拟机,它肯定是需要涉及到计算机的3大核心功能的:

  1. 方法调用:学过计算机的都知道,方法是作为程序组成的一个最基本的单元,而对于Java来说,原子指令其实就是字节码,Java方法也就是对字节码的封装,Java程序要想愉快的run起来,那JVM必须要支持对Java方法的调用;

  2. 取出指令:方法是对原子指令的封装,那最终在CPU上执行其实也就是指令逐条取出并执行,Java的方法执行也是一样的流程,这个时候就需要JVM配合了,JVM需要模拟CPU,逐条取出字节码指令并执行;

  3. 运算: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自定义的类型:


JVM系列之函数调用入门_第1张图片
CallStub定义

看到这里小伙伴们肯定不淡定了,这个call_stub函数不就是c语言里典型的函数指针么。指向的函数返回值类型是void,并且有8个入参,接下来我们就以call_stub函数的调用来依次做相关介绍。

注:c语言中相近的定义还有指针函数,但是它俩是完全不同的,一个重点是函数,一个重点是指针变量,具体的差别有兴趣的小伙伴请自行百度~~~

call_stub函数调用

JVM在javaCalls::call_helper()调用了该函数,我们来看看是怎么调用的:

注:javaCalls::call_helper()在javaCalls.cpp中实现

JVM系列之函数调用入门_第2张图片
CallStub调用

从源码可以看出,JVM隐式的调用了函数指针,我们来改一下这段源码,就一目了然了:


JVM系列之函数调用入门_第3张图片
修改后的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方法定义在globalDefinitions.hpp中

JVM系列之函数调用入门_第4张图片
castable_address实现

从源码可以看出,castable_address方法将入参x转换成了address_word类型:

注:address_word也是一个自定义类型,同样定义在globalDefinitions.hpp中

JVM系列之函数调用入门_第5张图片
address_word定义

从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函数可以继续替换成这样:


JVM系列之函数调用入门_第6张图片
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声明在subRoutines.hpp中

_call_stub_entry声明
  • _call_stub_entry初始化:
    在JVM初始化的时候,_call_stub_entry就会被初始化指向某一个内存地址,以Linux x86 64位系统为例:

注:_call_stub_entry初始化位于stubGenerator_x86_64.cpp中

JVM系列之函数调用入门_第7张图片
_call_stub_entry初始化

从加框部分源码可以看出来,_call_stub_entry是通过方法 generate_call_stub完成初始化的。

注:generate_call_stub方法中涉及到堆栈内存分配等操作,是JVM核心功能,下一篇文章会单独做详细分析介绍,在这里就不做深入介绍~

CallStub入参

在开始下一篇文章详细分析generate_call_stub()方法初始化_call_stub_entry之前,再做最后一个入门的基础知识介绍:CallStub的8个入参。
我们来回想上文中给出的JVM调用call_stub()方法源码,JVM在调用时一共传入了8个参数:

  1. link
    连接器,类型是JavaCallWrapper,我们来看一下该类型的定义,看看这个link到底想要链接谁~

    JVM系列之函数调用入门_第8张图片
    JavaCallWrapper定义

    从源码可以看出,JavaCallWrapper主要包含以下私有变量:

    • _thread:当前函数所在线程;

    • _handles:调用句柄;

    • _callee_method:调用者方法对象;

    • _receiver:被调用者;

    • _anchor:Java线程堆栈对象;

    • _result:方法返回值。

    通过这些私有变量可以看出,link主要连接了函数的调用者和被调用者~当然,函数在调用时,link指针也会被保存到当前方法的堆栈中。

  2. result_val_address
    函数返回值地址。

  3. result_type
    函数返回类型。

  4. method()
    当前方法在JVM中的表示对象。每一个方法在被加载的时候,JVM都会为其建一个模型,保存该方法所有的原始描述信息,主要包括:

    • 方法的名称,所属的类;

    • 方法的入参信息,包括入参类型,入参参数名,入参数量,顺序等;

    • 方法编译后的字节码信息,包括对应的字节码指令等;

    • 方法的注释信息;

    • 方法的继承信息;

    • 方法的返回信息

    method()参数的意义就是为了让JVM可以通过method()对象获取到Java方法编译后的字节码信息,JVM在拿到字节码后就可以解释执行了~

  5. entry_point
    JVM每次在调用Java函数时,必然会调用CallStub函数指针,当然咯,这个函数指针的值就是_call_stub_entry,JVM通过_call_stub_entry指向被调用函数地址,最终调用函数。在调用函数之前,必须要先经过entry_point,JVM实际是通过entry_point从method()对象上拿到Java方法对应的第一个字节码命令,这也是整个函数的调用入口。

  6. parameters()
    方法入参信息。JVM在调用函数之前,会通过该参数为函数分配堆栈,并将入参入栈。

  7. size_of_parameters()
    方法入参数量。

  8. CHECK
    当前线程对象。

到这里为止,函数调用一些入门的基础就介绍完了。下一章继续啃骨头,_call_stub_entry的初始化~

你可能感兴趣的