Jvm 执行引擎入门(一)入口

背景

Java为什么能够做到跨平台,因为c/c++等高级语言,一般是耦合在固定的操作系统或者依赖固定的指令集才可以运行,无法做到比如windows上运行,linux上面也同样可以运行,必然是要做一些操作系统函数上的更改。这就做不到兼容,而java可以做到,主要就是要与操作系统解耦,与寄存器指令集解耦,最好的办法就是使用一个中间语言,不直接调用某个操作系统的系统函数,但是由于中间语言并不是本地机器指令,机器CPU无法直接识别,因此中间语言并不能直接 由物理CPU运行,而是直接由虚拟机来解释中间语言,将中间语言直接翻译成对应平台的机器指令。而Java就必然要自己定义中间语言,就是Java的字节码。

执行引擎的工作原理

JVM作为一款虚拟机,也必然要涉及计算机核心的3大功能。

  1. 方法调用
    方法作为程序组成的基本单元,作为原子指令的初步封装,计算机必须能够支持方法的调 用。同样,Java语言的原子指令是字节码,Java方法是对字节码的封装,因此JVM必须支持对 Java方法的调用。

  2. 取指
    这里的“取指”,是指取出指令。
    还是那句话,方法是对原子指令的封装,计算机进入方法后,最终需要逐条取出这些指令 并逐条执行。Java方法也不例外,因此JVM进入Java方法后,也要能够模拟硬件CPU,能够 从Java方法中逐条取出字节码指令。

  3. 运算
    计算机取岀指令后,就要根据指令进行相应的逻辑运算,实现指令的功能。JVM作为虚拟 机,也需要具备对Java字节码的运算能力。

真实的机器调用

通过一个汇编讲解一个真实的机器调用的原理,一个真实的机器指令调用的机制基本上就是,现场保存,堆栈分配,参数传递等。


```bash
`main:
	//保存调用者栈基地址,并为main()函数分配新栈空间 
	pushl %ebp
	movl  %esp, %ebp
	subl$32, %esp	//这里就是分配新栈,一共32个字节
	//初始化两个数据,一个是5, 一个是3
	movl$5, 20 (%esp)
	movl$3, 24 (%esp)
	//压栈:将5和3压入栈中
	movl24 (%esp), %eax 
	movl %eax, 4 (%esp)
	movl20(%esp), %eax 
	movl%eax, (%esp)
	
	//调用add()函数
	call add 
	movl%eax, 28 (%esp) //得到add ()函数的返回结果
	
	//返回
	movl$ 0, %eax
	leave
	ret


add:
	subl$16, %esp
	movl12 (%ebp), %eax
	movl8 (%ebp), %edx
	addl%edx, %eax
	movl%eax, -4 (%ebp)
	movl-4 (%ebp), %eax
	leave
	ret

这个汇编很简单,就是对两个整数求和,整个步骤一共包含5步:

  1. 保存调用者栈基地址
  2. 初始化数据
  3. 压栈
  4. 函数调用
  5. 返回

要注意的就是,在物理机器执行call指令的时候,会自动将当前eip寄存器入栈,这样的话,当被调用者执行完之后,物理机器就再自动将eip出栈,这样执行完被调用函数之后,物理机器就会接着执行调用者的后续指令,物理机器执行函数调用时,被调用方也需要手动将ebp入栈。

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

jvm的函数调用机制

在源代码编码阶段就定义好一段机器指令,然后直接将一个C函数指针指向这段机器指令的首地址,从而间接实现C语言直接调用机器指令的目的 (其实,C语言还有一种办法可以调用汇编指令, 那就是内嵌汇编的方式,在Linux操作系统内核中就有大量的这种用法。但是这种用法与JVM 所要实现的目标稍微有点不同,JVM要实现直接由C语言调用机器指令,而内嵌汇编的方式只 实现了这一目标的一半,内嵌汇编的方式只能实现由C语言直接调用汇编指令。而汇编指令与机器指令之间还有很大差距(主要是因为汇编语言是一种高级语言,它使用符号来表示机器语言中的操作码和地址码。而机器语言直接使用二进制代码表示操作,这些代码对应于计算机硬件的实际操作。因此,将汇编语言转换为机器语言的过程需要进行地址分配、操作码转换等一系列步骤,这会导致一些开销和延迟。)

Java字节码指令直接对应一段特定逻 辑的本地机器码,而JVM在解释执行Java字节码指令时,会直接调用字节码指令所对应的本 地机器码。JVM是使用C/C++编写而成的,因此JVM要直接执行本地机器码,便意味着必须 要能够从C/C++程序中直接进入机器指令。这种技术实现的关键便是使用C语言所提供的一种 高级功能一函数指针。通过函数指针能够直接由C程序触发一段机器指令。
在JVM内部,call_stub便是实现C程序调用字节码指令的第一步一例如Java主函数的 调用。在JVM执行Java主函数所对应的第一条字节码指令之前,必须经过call_stub函数指针 进入对应的例程,然后在目标例程中触发对Java主函数第一条字节码指令的调用。

执行流程

C调用java方法主要是通过JavaCalls::call()、JavaCalls::call_helper()等函数调用。这些函数定义在JavaCalls类中

源代码位置:openjdk/hotspot/src/share/vm/runtime/javaCalls.hpp
 
class JavaCalls: AllStatic {
  static void call_helper(JavaValue* result, methodHandle* method, JavaCallArguments* args, TRAPS);
 public:
  
  static void call_default_constructor(JavaThread* thread, methodHandle method, Handle receiver, TRAPS);
 
  // 使用如下函数调用Java中一些特殊的方法,如类初始化方法<clinit>等
  // receiver表示方法的接收者,如A.main()调用中,A就是方法的接收者
  static void call_special(JavaValue* result, KlassHandle klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
 
  // 使用如下函数调用动态分派的一些方法
  static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
 
  // 使用如下函数调用Java静态方法
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS);
   static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
 
  // 更低一层的接口,如上的一些函数可能会最终调用到如下这个函数
  static void call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS);
};

Java虚拟机规范定义的字节码指令共有5个,分别为invokestatic、invokedynamic、invokeinterface、invokespecial、invokevirtual几种方法调用指令。这些call_static()、call_virtual()函数内部调用了call()函数。看一下重要的main()方法来查看具体的调用逻辑

main()
-> //... 做一些参数检查
-> //... 开启新线程作为main线程,让它从JavaMain()函数开始执行;该线程等待main线程执行结束


JavaMain()
-> //... 找到指定的JVM
-> //... 加载并初始化JVM
-> //... 根据Main-Class指定的类名加载JavaMainClass
-> //... 在JavaMainClass类里找到名为"main"的方法,签名为"([Ljava/lang/String;)V",修饰符是public的静态方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通过JNI调用JavaMainClass.main()方法

调用Java主类main方法时会经过的主要方法及执行的主要流程

在这里插入图片描述

// HotSpot VM里对JNI的CallStaticVoidMethod的实现。留意要传给Java方法的参数
// 以C的可变长度参数传入,这个函数将其收集打包为JNI_ArgumentPusherVaArg对象
-> jni_CallStaticVoidMethod()  
 
     // 这里进一步将要传给Java的参数转换为JavaCallArguments对象传下去    
     -> jni_invoke_static()  
       
        // 真正底层实现的开始。这个方法只是层皮,把JavaCalls::call_helper()
        // 用os::os_exception_wrapper()包装起来,目的是设置HotSpot VM的C++层面的异常处理
        -> JavaCalls::call()   
     
           -> JavaCalls::call_helper()
              -> //... 检查目标方法是否为空方法,是的话直接返回
              -> //... 检查目标方法是否“首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法
              -> //... 获取目标方法的解释模式入口from_interpreted_entry,下面将其称为entry_point
              -> //... 确保Java栈溢出检查机制正确启动
              -> //... 创建一个JavaCallWrapper,用于管理JNIHandleBlock的分配与释放,
                 // 以及在调用Java方法前后保存和恢复Java的frame pointer/stack pointer

              //... StubRoutines::call_stub()返回一个指向call stub的函数指针,
              // 紧接着调用这个call stub,传入前面获取的entry_point和要传给Java方法的参数等信息
              -> StubRoutines::call_stub()(...) 
                 // call stub是在VM初始化时生成的。对应的代码在
                 // StubGenerator::generate_call_stub()函数中
                 -> //... 把相关寄存器的状态调整到解释器所需的状态
                 -> //... 把要传给Java方法的参数从JavaCallArguments对象解包展开到解释模式calling convention所要求的位置
                 -> //... 跳转到前面传入的entry_point,也就是目标方法的from_interpreted_entry

                 -> //... 在-Xcomp模式下,实际跳入的是i2c adapter stub,将解释模式calling convention
                     // 传入的参数挪到编译模式calling convention所要求的位置
                           -> //... 跳转到目标方法被JIT编译后的代码里,也就是跳到 nmethod 的 VEP 所指向的位置
                                -> //... 正式开始执行目标方法被JIT编译好的代码 <- 这里就是"main()方法的真正入口"

由上可知,HotSpot VM是通过JavaCalls::call()函数来间接调用main()方法的

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值