java虚拟机详细图解9--JVM机器指令集

摘要:
本文将通过一个非常简单的例子,带你感受一下Java虚拟机运行机器码的过程和其工作的基本原理。如何读懂class二进制文件中关于method及其相应机器码的组织,请阅读《Java虚拟机原理图解》1.5、class文件中的方法表集合--method方法在class文件中是怎样组织的。

声明:本文摘抄自:https://blog.csdn.net/u010349169/article/details/50412126

Java虚拟机和真实的计算机一样,运行的都是二进制的机器码;而我们将.java 源代码编译成.class 文件,class文件便是Java虚拟机能够认识的二进制机器码,Java能够识别class文件中的信息和机器指令,进而执行这些机器指令。那么,Java虚拟机是如何运行这些二进制的机器码的呢? 本文将通过一个非常简单的例子,带你感受一下Java虚拟机运行机器码的过程和其工作的基本原理。
读完本文,你将会了解到:

  1、Java虚拟机对运行时虚拟机栈(JVM Stack) 的组织

  2、方法调用过程是怎样在JVM中表示的

  3、JVM对一个方法执行的基本策略

  4. JVM机器指令的格式

  5. 机器指令的执行模式---基于操作数栈的模式

1. Java虚拟机对运行时虚拟机栈(JVM Stack)的组织

Java虚拟机在运行时会为每一个线程在内存中分配了一个虚拟机栈,来表示线程的运行状态和信息,虚拟机栈中的元素称之为栈帧(JVM stack frame),每一个栈帧表示这对一个方法的调用信息。如下所示:

java虚拟机详细图解9--JVM机器指令集第1张

  上述的描述可能会有点抽象,为了给读者一个直观的感受,我们定义一个简单的Java类,然后运行这个类,逐步分析整个Java虚拟机的运行时信息的组织的。

2. 方法调用过程在JVM中是如何表示的

我们将定义如下带有main方法的简单类org.louis.jvm.codeset.Bootstrap.java ,逐步分析该类在JVM中是如何表示的,方法是如何一步步运行的:

java虚拟机详细图解9--JVM机器指令集第2张

当我们将Bootstrap.java 编译成Bootstrap.class 并运行这段程序的时候,在JVM复杂的运行逻辑中,会有以下几步:

  1. 首先JVM会先将这个Bootstrap.class 信息加载到 内存中的方法区(Method Area)中。

     Bootstrap.class 中包含了常量池信息,方法的定义 以及编译后的方法实现的二进制形式的机器指令,所有的线程共享一个方法区,从中读取方法定义和方法的指令集。

  2. 接着,JVM会在Heap堆上为Bootstrap.class 创建一个Class<Bootstrap>实例用来表示Bootstrap.class 的 类实例。

  3. JVM开始执行main方法,这时会为main方法创建一个栈帧,以表示main方法的整个执行过程(我会在后面章节中详细展开这个过程);

  4. main方法在执行的过程之中,调用了greeting静态方法,则JVM会为greeting方法创建一个栈帧,推到虚拟机栈顶(我会在后面章节中详细展开这个过程)。

  5.当greeting方法运行完成后,则greeting方法出栈,main方法继续运行;

java虚拟机详细图解9--JVM机器指令集第3张

JVM方法调用的过程是通过栈帧来实现的,那么,方法的指令是如何运行的呢?弄清楚这个之前,我们要先了解对于JVM而言,方法的结构是什么样的。

  我们知道,class 文件是 JVM能够识别的二进制文件,其中通过特定的结构描述了每个方法的定义。

JVM在编译Bootstrap.java 的过程中,在将源代码编译成二进制机器码的同时,会判断其中的每一个方法的三个信息:

  1 ). 在运行时会使用到的局部变量的数量(作用是:当JVM为方法创建栈帧的时候,在栈帧中为该方法创建一个局部变量表,来存储方法指令在运算时的局部变量值)

  2 ). 其机器指令执行时所需要的最大的操作数栈的大小(当JVM为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作)

  3 ). 方法的参数的数量

  经过编译之后,我们可以得到main方法和greeting方法的信息如下:

java虚拟机详细图解9--JVM机器指令集第4张

注: 上述编译后的信息全部都存储在Bootstrap.class 文件中,并按照这Class文件格式的形式存储,关于Class文件格式的定义,我在前几篇文章中已经做了非常详尽的介绍,如果您全部阅读了,那么相信您已经可以“读懂” class 文件了。如何读懂class二进制文件中关于method及其相应机器码的组织,请阅读《Java虚拟机原理图解》1.5、 class文件中的方法表集合--method方法在class文件中是怎样组织的。

JVM运行main方法的过程:

  1.为main方法创建栈帧:

  JVM解析main方法,发现其局部变量的数量为 2,操作数栈的数量为1, 则会为main方法创建一个栈帧(VM Stack),并将其加入虚拟机栈中:

java虚拟机详细图解9--JVM机器指令集第5张

 2. 完成栈帧初始化:

    main栈帧创建完成后,会将栈帧push 到虚拟机栈中,现在有两步重要的事情要做:

    a). 计算PC值。PC 是指令计数器,其内部的值决定了JVM虚拟机下一步应该执行哪一个机器指令,而机器指令存放在方法区,我们需要让PC的值指向方法区的main方法上;

初始化PC = main方法在方法区指令的地址+0;

    b). 局部变量的初始化。main方法有个入参(String[] args) ,JVM已经在main所在的栈帧的局部变量表中为其空出来了一个slot ,我们需要将 args 的引用值初始化到局部点亮表中;

java虚拟机详细图解9--JVM机器指令集第6张

 接着JVM开始读取PC指向的机器指令。如上图所示,main方法的指令序列:12 10 4c 2b b8 20 12 b1,通过JVM虚拟机指令集规范,可以将这个指令序列解析成以下Java汇编语言:

机器 指令汇编语言解释对栈帧的影响
0x12 0x10ldc #16将常量池中第16个常量池项引用推到操作数栈栈顶。
常量池第16项是CONSTANT_UTF-8_INFO项,表示”Louis”字符串
java虚拟机详细图解9--JVM机器指令集第7张
0x4castore_1

操作数栈的栈顶元素出栈,将栈顶元素的值赋给index=1 的局部变量表元素上。

这里等价于:name = “Louis”.

java虚拟机详细图解9--JVM机器指令集第8张
0x2baload_1将局部变量表中index=1的元素的值推到操作数栈栈顶java虚拟机详细图解9--JVM机器指令集第9张
0xb8 0x20 0x12invokestatic #18

0xb8表示机器指令invokestatic,操作数是0x20 << 8| 0x12 = 18,操作数18表示指向常量池第18项,该项是main方法的符号引用:

org/louis/jvm/codeset/Bootstrap.greeting:(Ljava/lang/String;)V

当JVM执行这条语句的时候,会做以下几件事:

a).方法符号引用校验。会校验这个方法的符号引用,按照这个符号规则 在常量池中查找是否有这个方法的定义,如果找到了此方法的定义,则表示解析成功。如果是方法greeting:(Ljava/lang/String;)V没有找到,JVM会抛出错误NoSuchMethodError

b).为新的方法调用创建新的栈帧。然后JVM会为此方法greeting创建一个新的栈帧(VM stack),并根据greeting中操作数栈的大小和局部变量的数量分别创建相应大小的操作数栈;然后将此栈帧推到虚拟机栈的栈顶。

c).更新PC指令计数器的值。将当前PC程序计数器的值记录到greeting栈帧中,当greeting执行完成后,以便恢复PC值。更新PC的值,使下一条执行的指令地址指向greeting方法的指令开始部分。

这条语句会使当前的main方法执行暂停,使JVM进入对greeting方法的执行当中当greeting方法执行完成后,才会恢复PC程序计数器的值指向当前下一条指令。

0xb1return返回

  

  当main方法调用greeting()时, JVM会为greeting方法创建一个栈帧,用以表示对greeting方法的调用,具体栈帧信息如下:

  java虚拟机详细图解9--JVM机器指令集第10张

具体的greeting方法的机器码表示的含义如下图所示:

机器指令汇编语言解释常量池引用
b2 20 1agetstatic #26获取指定类的静态域,并将其值压入栈顶.
将常量池中的第26个符号引用推到操作数栈中:
#26:
// Field java/lang/System.out:Ljava/io/PrintStream;
bb 20 20new #32创建一个对象,并将其引用值压入栈顶。
创建一个java/lang/StringBuider实例,将其压入栈顶。
#32:
// class java/lang/StringBuilder
59dup复制操作数栈栈顶的值,并插入到栈顶
12 22ldc #34从运行时常量池中提取数据推入操作数栈
将“Hello” String引用复制到 操作数栈中
#34:
// String Hello,
b7 20 24invokespecial #36调用超类构造方法,实例初始化方法,私有方法。
此处调用StringBuilder(String)构造方法,并将结果推到栈顶
#36:
// Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
2ainvokevirtual #38调用超类构造方法,实例初始化方法,私有方法。
StringBuilder实例的 append(String ) 方法,表示:
"Hello,"+"Louis".
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
b6 20 2ainvokevirtual #42调用超类构造方法,实例初始化方法,私有方法。
调用StringBuilder实例的toString()方法,结果保留在栈顶。
// Method java/lang/StringBuilder.toString:()Ljava/lang/String;
b6 20 2einvokevirtual #46调用超类构造方法,实例初始化方法,私有方法。
调用System.out.println(String)方法
// Method java/io/PrintStream.println:(Ljava/lang/String;)V
b1return结束返回

3. JVM对一个方法执行的基本策略

  一般地,对于java方法的执行,在JVM在其某一特定线程的虚拟机栈(JVM Stack) 中会为方法分配一个 局部变量表,一个操作数栈,用以存储方法的运行过程中的中间值存储。

由于JVM的指令是基于栈的,即大部分的指令的执行,都伴随着操作数的出栈和入栈。所以在学习JVM的机器指令的时候,一定要铭记一点:

每个机器指令的执行,对操作数栈和局部变量的影响,充分地了解了这个机制,你就可以非常顺畅地读懂class文件中的二进制机器指令了。

  如下是栈帧信息的简化图,在分析JVM指令时,脑海中对栈帧有个清晰的认识:

java虚拟机详细图解9--JVM机器指令集第11张

4. 机器指令的格式

  所谓的机器指令,就是只有机器才能够认识的二进制代码。一个机器指令分为两部分组成:

  java虚拟机详细图解9--JVM机器指令集第12张

注:

    a). 如上图所示JVM虚拟机的操作码是由一个字节组成的,也就是说对于JVM虚拟机而言,其指令的数量最多为 2^8,即 256个;

    b). 上图中的操作码如:b2,bb,59....等等都是表示某一特定的机器指令,为了方便我们识别,其分别有相应的助记符:getstatic,new,dup.... 这样方便我们理解。

5. 机器指令的执行模式---基于操作数栈的模式

  对于传统的物理机而言,大部分的机器指令的设计都是寄存器的,物理机内设置若干个寄存器,用以存储机器指令运行过程中的值,寄存器的数量和支持的指令的个数决定了这个机器的处理能力。

  但是Java虚拟机的设计的机制并不是这样的,Java虚拟机使用操作数栈 来存储机器指令的运算过程中的值。所有的操作数的操作,都要遵循出栈和入栈的规则,所以在《Java虚拟机规范》中,你会发现有很多机器指令都是关于出栈入栈的操作。

  java虚拟机详细图解9--JVM机器指令集第13张

本文旨在介绍JVM虚拟机指令的运行原理,如果你想更深入地了解指令集的信息以及使用注意事项,请您阅读《Java虚拟机规范(Java Virtual Machine Specification)》 关于机器指令集的详细定义。

免责声明:文章转载自《java虚拟机详细图解9--JVM机器指令集》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Vue(三)-- class与style绑定、条件渲染、列表渲染、列表的搜索和排序kafka2.5.0 主题Topic下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Xamarin 跨移动端开发系列(01) -- 搭建环境、编译、调试、部署、运行

   (本文是基于老版本的VS和Xamarin,而VS2017已经集成了Xamarin,所以,本文已经过时,最新的Xamarin开发介绍请参见 使用 Xamarin开发手机聊天程序 。)    如果是.NET开发人员,想学习手机应用开发(Android和iOS),Xamarin 无疑是最好的选择,编写一次,即可发布到Android和iOS平台,真是利器中的...

深入理解JavaScript闭包

一、变量的作用域   要理解闭包,首先必须理解Javascript特殊的变量作用域。变量的作用域无非就是两种:全局变量和局部变量。Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。另一方面,在函数外部自然无法读取函数内的局部变量。(这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全...

VMware vSphere 虚拟化平台的安装及使用

首先解释一下这些名词, vSphere是什么? vSphere 是VMware公司发布的一整套产品包,是VMware公司推出的一套服务器虚拟化解决方案,包含VMware ESXi hypervisor,VMware vCenter Server等产品 ESXi是什么? ESXi是一个hypervisor(虚拟机管理程序),就是一个类似于Xen的虚拟层,用于...

深入理解计算机系统(4.1)---X86的孪生兄弟,Y86指令体系结构

引言 各位猿友们好,计算机系统系列很久没更新了,实在是抱歉之极。新的一年,为了给计算机系统系列添加一些新的元素,LZ将其更改为书的原名《深入理解计算机系统》。这本书非常厚,而且难度较高,LZ看了很久才看了四章。当然,这跟LZ最近很久没翻书有关系,最近公司的事情比较多,可让LZ愁了个愁,尤其是招人的事一直不太顺利,很多工作无法展开,也让LZ的心中一直压着一...

C# Dictionary.Keys用法及代码示例

此属性用于获取包含Dictionary中的键的集合。 用法: public System.Collections.Generic.Dictionary<TKey, TValue>.KeyCollection Keys { get; } 返回值:它返回一个包含Dictionary中关键字的集合。 以下示例程序旨在说明上面讨论的属性的使用:...

函数对象与仿函数(function object and functor)

part 1. 仿函数在STL组件中的关系   如下图:   # 仿函数配合算法完成不同的策略变化。   # 适配器套接仿函数。 part 2. 仿函数介绍   传递给算法的“函数型实参”不一定得是函数,可以是行为类似函数的对象。这种对象称为函数对象(function object),或称为仿函数(functor)。——《STL标准库(第2版)》 P23...