java执行引擎工作原理

 

1.JVM作为一款虚拟机,必然涉及计算机核心的三大功能。

1,方法调用:

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

2,取指:

这里的“取指”是指取出指令。

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

3.运算:

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

 

2.真实的机器调用。

1.下面通过一个汇编程序讲解一些真实的机器调用原理,若看不太懂也没关系,大多看几遍就行,为什么要了解这些,因为JVM就是模拟机器的内存分配与指令调用,并且,下面给出了详细的内存分析图解,可以帮助我们去了解物理机器的内存图。(曾经我就因为看到网上各式各样的内存图却分不清哪个是正确而纠结)。

     真实的机器指令调用涉及的知识较多,例如,现场保存,堆栈分配,参数传递,等等,了解了这些最底层的原理,那么再理解JVM的函数调用就会变得简单。看下面的示例:对两个整数求和(相信使用机器指令书写没人看的懂,这里使用机器指令助记符----汇编语言)

Java 工作流引擎有哪些 java工作流引擎原理_jvm

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_02

我相信只要没有汇编语言基础的人,都是看不懂的,其实我也看不懂,既然是对两个整数的求和,大致可以看到两个方法,一个main(),一个add(),那么也就能猜到在main中定义两个变量,由add执行加法运算,即使对汇编语言不了解,那么我们只需要关注最终内存是如何变化的就行了。

下面是对main函数详解:

 

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_03

 

Java 工作流引擎有哪些 java工作流引擎原理_java_04

上面main()函数代码注释一共包括五个步骤:保存调用者栈基地址,初始化数据,压栈,函数调用和返回。下面分析一下:

1.保存栈基并分配新栈:

看main函数的第一步

Java 工作流引擎有哪些 java工作流引擎原理_jvm_05

pushl %ebp 就是保存调用者的栈基地址,调用者是谁,谁能调用一个程序的主函数,自然是操作系统,movl %esp, %ebp将调用者的栈基地址指向其栈顶。这两句是所有函数调用时都会必定会执行的指令。

执行完上面两句后,看下面这条指令

Java 工作流引擎有哪些 java工作流引擎原理_java_06

这条指令就是分配栈空间,对于物理机器而言,分配栈空间就是非常容易的一件事,这条指令中的subl表示减,指令中的%esp表示当前栈顶,整条指令的含义就是当前栈顶减去32个字节的长度,为什么是减,因为在liunx平台上,栈是向下增长的,从内存的高地址往低地址增长,每次调用一个新的函数时,需要为新的函数分配栈空间,新函数的栈顶相对于调用者函数的栈顶,内存地址一定是低位方向,因此新函数的栈顶总是通过对调用者函数的栈顶做减法而计算出来的。

执行完这条指令后,main()函数就有了自己的方法栈,栈空间大小是32字节,一个字节包含8个二进制位,如果一个int类型的整数包含4个字节的话,那么main()函数的方法栈一共可以容下8个int类型的数据main()函数初始化堆栈如下图

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_07

mian()函数执行完上面三条命令,便完成了调用者栈基保存和自身栈空间的分配。

2.初始化数据

接下来两条指令:

Java 工作流引擎有哪些 java工作流引擎原理_jvm_08

很明显,这两条指令含义是将整数5,3保存到main()函数的栈中,其中20(%esp)表示当前栈顶(即esp寄存器当前所指向的内存地址)往上移动20字节的位置,数据5保存在这里,同理整数3保存在mian()函数栈顶往上偏移24字节处的位置,由于一个整数占4个字节,因此5和3被分别保存在main()方法栈顶往上偏移5个整数和六个整数的位置。

我们将main()函数的栈顶标记为(%esp),那么main函数的方法栈内存图如下所示

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_09

这个时候我们再来看看5和3再main()方法栈中的位置

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_10

到这里大家可能有些疑问,为什么5和3被分配到中间的位置上呢,而不是在最上面或者最下面?请看下面:

  现在main()函数完成了数据准备,接着便要调用add()函数进行求和了。

接着main()函数开始执行下面四条指令:

Java 工作流引擎有哪些 java工作流引擎原理_jvm_11

这四条指令的作用主要是压栈,前两条指令是把数据3压栈,后两条指令把数据5压栈。

movl 24 (%esp),%eax这条指令是将24 (%esp)处的内存值传送到 eax 寄存器中,这个值就是整数3,接着movl %eax,4(%esp)这条指令又将eax寄存器中的值传送到4(%esp)这个位置,同理5被传送到(%esp)这个位置,就是栈顶。

在这里可能会有个疑问,为什么不直接将24(%esp)处的数据直接传送到4(%esp)位置上,原因是CPU无法完成内存之间的数据传送。

这时 main()函数的内存布局如下图所示:

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_12

一般而言,往栈顶传送数据的行为,叫做压栈。真实的物理机器,在发起函数调用之前,必定要进行压栈,压栈的目的是为传参。main()函数在这里压栈的两个数据,将会被add()函数读取到。

对于物理机器而言,函数调用就特别简单了,只有一条命令

Java 工作流引擎有哪些 java工作流引擎原理_java_13

add()函数计算完成之后,会将计算的结果保存到 eax 寄存器中。main()函数调用以下命令获取add()函数的返回值。

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_14

通过这条指令,main()函数完成了求和计算,此时main()函数的内存图

Java 工作流引擎有哪些 java工作流引擎原理_jvm_15

这样的布局是不是感觉很完美,这些全是编译器的功劳,编译器会将一个方法的局部变量分配在靠近栈底的位置,而将传递的参数分配在靠近栈顶的位置。

main()函数返回指令就比较简单了,将返回值保存在eax寄存器中,然后执行两条返回指令,就大功告成。

Java 工作流引擎有哪些 java工作流引擎原理_Java 工作流引擎有哪些_16

 

add()函数详解:

 

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_17

上述可以看到,add()函数总体分为四个步骤,保存调用者栈基地址,读取入参,执行运算,返回。

1.保存调用者栈基地址

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_18

物理机器在执行函数调用时,被调用者总是要保存调用者的栈基地址。这是因为esp和ebp这两个寄存器接下来要指向被调用者的栈基地址和栈顶,这两个寄存器原本保存的是调用者的栈基地址和栈顶地址,现在即将被修改,如果不保存起来,那么当被调用者函数执行完成之后,程序流返回到调用者流程中时,物理机器将无法恢复调用者的栈基和栈顶,从而导致程序无法继续执行。

add()函数第三条指令

Java 工作流引擎有哪些 java工作流引擎原理_jvm_19

分配栈空间,大小为16个字节,现在来看看方法栈空间的内存布局,由于add()函数的方法栈是在调用方main()函数的方法栈空间基础上继续往下增长的,并且add()方法栈与main()方法栈连在一起的,现在我们看一下

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_20

我们可以看到main()函数栈与add()函数栈的中间多出来8个字节eip和ebp寄存器。原因是在main()函数执行call add 指令时,物理机器自动往栈顶压了一个数值----eip。前面我们说过CPU所执行的指令位置由CS:IP这两个段寄存器共同决定,这里的eip就是IP寄存器。物理机器为何要将这个寄存器的值入栈,主要是为了让main()函数执行完call调用回来之后,能够继续处理main()函数中接下来的指令,如果没有call add这条指令,即main()函数不调用add()函数,那么main()函数执行完call add这条指令的上一条指令movl %eap,(%esp)之后,main()函数应该接着执行call add这条指令的后面一条指令movl %eax,28(%esp),同时main()函数在执行movl %eax,28(%esp)这条指令之前,eip寄存器需要先指向这条指令,这样CPU才能读取到这条指令并执行。但现在main()函数在执行这条指令之前,先调用了add()函数,那么eip就会指向add()函数里面的指令内存地址(这一步是物理机器自动执行的),那么当 add()函数执行完其最后一条指令,物理机器怎么知道接下来要把eip寄存器指向哪里?所以,执行完一个函数,我们不去修改eip寄存器的值,那么物理机器根本就不知道接下来应该怎么做。基于这样的原因,在执行函数调用时,CPU设计者在里面加了一个功能,即在物理机器执行call 指令时,自动将当前eip寄存器入栈,而当被调用者完成之后,物理机器再自动将eip出栈,这样执行完被调用函数之后,物理机器会紧接着执行调用者的后续指令。

 上述解释了main()函数和add()函数中间多出来的8字节中的eip 4个字节的原因。通过上图 我们可以看到剩下四个字节存放ebp的值,这个值是在执行add()函数时入栈的。我们看,add()函数的开头第一条指令就是pushl %ebp,这里显式执行了入栈操作。

经过上述分析可以得出:

 1.物理机器执行call函数调用时,机器会自动将eip入栈。

 2.物理机器执行函数调用时,被调用方需要手动将ebp入栈。

 

2.读取入参:add()函数第四和第五条指令为读取入参指令。

Java 工作流引擎有哪些 java工作流引擎原理_jvm_21

由于add()函数一开始执行了movl %esp,%ebp指令,此时ebp寄存器已经指向原来main()函数的栈顶,第一条指令合起来的意思就是,从add()函数栈底向上偏移12个字节的位置取出数据传送给eax寄存器。第二条指令同理将数据传送给edx寄存器。此时看一下内存图

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_22

之前在main()函数执行压栈操作时,当时压栈的位置是(%esp)和4(%esp),而现在这两个位置变成了8(%esp)和12(%esp),由此可见,随着堆栈寄存器ebp,esp所指向位置的变化,方法栈中同一个位置的偏移量也随之改变。对于压栈的入参,可通过调用者函数的栈顶偏移量来相对定位,也可以通过相对于被调用者的函数栈底来相对定位。但是对于被调用者函数的方法栈内的数据,不能以调用者函数为基准通过偏移量获取,因为此时被调用者函数尚未分配方法栈空间。

3,执行运算

 

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_23

这条指令的含义是将edx寄存器中的值和eax寄存器中的值相加,结果保存到eax寄存器中。在add()函数执行本指令之前,已经通过读取入参指令将main()函数所传递过来的两个参数分别读取到了eax和edx寄存器中,因此对两个寄存器执行求和运算。执行完求和运算,add()函数执行

Java 工作流引擎有哪些 java工作流引擎原理_寄存器_24

这条指令的作用是把eax寄存器中的值转移到栈基地址往下偏移4个字节位置,这个位置就是add()函数方法栈内第一个位置,由此可见 add()函数将求和的值保存在房发展第一个位置,内存图如下

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_25

图中可以看到 计算结果可以通过 -4(%ebp)相对定位也可以通过12(%esp)相对定位。

4.返回

  执行完求和运算,接下来该返回了,返回逻辑一般是有返回值,就把返回值放在eax寄存器中,然后执行leave和ret指令。如果没有,就直接执行leave和ret指令。

至此,add()函数全部执行结束,程序流返回到main()函数中,程序流是如何返回到main()函数中的呢?前面说过,物理机器想要执行某个函数的指令,必须先把ip段寄存器指向那个函数,ip指向哪里,物理机器就执行到哪里,在add函数的返回leave指令中顺带执行了下面两个指令:

Java 工作流引擎有哪些 java工作流引擎原理_函数调用_26

 在main()函数调用add()函数时,物理机器执行了push %eip指令,将main()函数中add指令的下一条指令的内存地址入栈,现在add()函数执行结束了,再把这个地址出栈,保存到eip寄存器中,这样eip寄存器又指向main()函数中的add指令的下一条指令了,当然,在弹出eip之前,得通过mov %ebp,%esp指令将栈顶位置指向栈基,这样栈顶位置就是eip。

总结

   通过对这段汇编程序的分析,物理机器在执行程序时,将程序划分为若干函数,每个函数都对应有一段机器码,一段机器码都放在一段连续的内存中,这块内存叫做代码段。物理机器为每一个函数分配一个方法栈,方法栈与代码段在地址上没有任何关系,只有当物理机器执行到某个函数时,才会为其分配方法栈。函数通过自身的一条条机器指令,操作方法栈。其实 JVM也是java函数对应的机器指令专门存储在内存的一块区域上,同时为每一个java函数分配了方法栈。