第七章 -- JVM体系结构与工作方式 ( 许令波 )
7.1 -- JVM体系结构
7.1.1 什么是JVM ?
宏观角度分析一下最基本的JVM结构和工作模式
JVM全称是 Java Virtual Machine (Java虚拟机),他通过模拟一个计算机来达到一个计算机所具有的计算功能.我们先来看看一个真实的计算机如何才能具有计算的功能?
以计算为中心来看计算机的体系结构可以分为如下几个部分 :
指令集 -- 这个计算机所能识别的机器语言的命令集合
计算单元 -- 能够识别并且控制指令执行的功能模块
寻址方式 -- 地址的位数,最小地址和最大地址范围,以及地址的运行规则
寄存器定义 -- 包括操作数寄存器,变址寄存器,控制寄存器等的定义,数量和使用方式.
存储单元 -- 能够存储操作数和保存操作结构的单元,如内核级缓存,内存和磁盘等
上面几个部分中与代码执行最密切的还是指令集. 那么计算机中的指令集是如何定义的?
什么是指令集?有何作用?
所谓指令集就是CPU中用来计算和控制计算机系统的一套指令的集合 ,每一种新型的CPU在设计时都规定了一系列与其他硬件电路相配合的指令系统 . 而指令系统的先进与否也关系到CPU的性能发挥 ,它是体现CPU性能的一个重要标志 . 当前计算机中有哪些指令集?从主流的体系结构上分为精简指令集RISC和复杂指令集CISC.当前我们普遍使用的桌面操作系统中基本上使用的都是CISC.除了这两种外,还在它们的基础上开发出了很多扩展指令集 .
指令集与汇编语言有什么关系 ?
指令集是可以直接被机器识别的机器码,它必须以二进制格式存在于计算机中.
汇编语言是能够被人识别的指令,汇编语言在顺序和逻辑上是与机器指令 一 一对应的.汇编语言是为了让人能够更容易地记住机器指令而使用的助记符.
每一条汇编指令都可以直接翻译成一个机器指令.但是也不是所有的汇编语言都有对应的机器指令,如nop指令.
指令集与CPU架构有什么联系?CPU的架构是否会影响指令集?
答案是肯定的.学过汇编语言都知道, 汇编语言中都是对寄存器和段的直接操作的命令, 这些寄存器和段等芯片都是架构的一部分, 所以不同的芯片架构设计一定会对应到不同的机器指令集合. 但是现在的不同芯片厂商往往都会采用兼容的方式来兼容其他不同架构的指令集. 如AMD会兼容32位Intel的x86系统架构的CPU,而当AMD开发出了支持64位指令的X86-64架构,Intel又迫于压力不得不箭筒这种架构而起了另外一个名字EM64T. 这种压力来自什么地方? 当然是垄断了操作系统的微软. 由于现在操作系统是管理计算机的真正入口, 几乎所有的程序都要通过操作系统来调用, 所以如果操作系统不支持某种芯片的指令集, 用户的程序时不可能执行的. 这种情况也存在于我们国家自己设计的龙芯CPU, 龙芯CPU不得不使用基于MIPS架构的指令集(RISC), 因为目前有直接支持MIPS架构的操作系统(Linux). 如果没有操作系统和应用软件, 再好的CPU也没有使用价值.
JVM和实体机到底有何不同呢?
大体有如下几点:
- 一个抽象规范,这个规范就约束了JVM到底是一个什么东西,它有哪些组成部分,这些抽象的规范都在the java virtual machine sspecifictin书中详细描述了.
- 一个具体的实现,所谓具体的实现就是不同的厂商按照这个抽象的规范用软件或者软硬结合的方式在相同或者不同的平台上的具体的实现
- 一个运行中的实例,当用这个东西运行一个java程序时,它就是一个运行中的实例,每个运行中的java程序都是一个JVM实例
JVM和实体机一样也必须有一套合适的指令集,这个指令集能够被JVM解析执行,这个指令集我们称为JVM字节码指令集,只要符合class文件规范的字节码都可以被JVM执行
7.1.2 JVM体系结构详解
基本由4部分组成 :
- 类加载器 : 在JVM启动时或者在类运行时将需要的class加载到JVM中
- 执行引擎 : 负责执行class文件中包含的字节码指令,相当于实际机器上的cpu
- 内存区 : 将内存划分为若干个区以模拟实际机器上的存储 ,记录和调度功能模块,如实际机器上的各种功能的寄存器或者PC指针的记录器等.
- 本地方法调用 : 调用C或C++实现的本地方法代码返回结果
类加载器 :
每一个被JVM装载的类型都有一个对应的Java.lang.class 类的实例来表示该类型,该实例可以唯一表示被JVM加载的class类,要求这个实例和其他类实例一样都存放在Java的堆中
执行引擎 :
执行引擎是JVM核心部分 , 作用就是解析JVM字节码指令,得到执行结果.在<Java虚拟机规范>中详细地定义了执行引擎遇到每条字节码指令时应该处理什么,并且应该得到什么结果.但是并没有规定执行引擎应该如何或采取什么方式处理而得到这个结果.因为执行引擎具体采用什么方式由JVM的实现厂家自己去实现,是直接解释执行还是采用JIT技术转成本地代码去执行, 还是采用寄存器这个芯片模式去执行都可以.所以执行引擎的具体实现有很大的发挥空间,如SUN的hotspot是基于栈的执行引擎,而Google的Dalvik是基于寄存器的.执行引擎也就是执行一条条代码的流程,而代码都是包含在方法体内的,所以执行引擎本质上就是执行一个个方法串起来的流程,对应到操作系统中一个执行流程是一个Java进行还是一个Java线程呢?很显然是线程,因为一个Java进程可以有多个同时执行的执行流程.这样说来每一个java线程就是一个执行引擎的实例, 那么一个JVM实例中就会同时有多个执行引擎在工作,这些执行引擎有的在执行用户程序,而有的在执行JVM内部程序(如Java垃圾收集器).
Java内存管理:
执行引擎在执行一段程序时需要存储一些东西,如操作码需要的操作数,操作码的执行结果需要保存.class类的字节码还有类的对象等这些信息都需要在执行引擎执行之前就准备好.从图中可以看出一个JVM实例会有一个方法区,Java堆,Java栈,PC寄存器和本地方法区.其中方法区和堆是所有线程共享的,也就是可以被所有的执行引擎实例访问.每一个新的执行引擎实例被创建时会为这个执行引擎创建一个java栈和一个PC寄存器,如果当前正在执行一个Java方法,那么当前的这个Java栈中保存的是该线程中方法调用的状态,包括方法的参数,局部变量,返回值以及运算中间结果等.而PC寄存器会指向即将执行的下一条指令.
如果是本地方法调用则存储在本地方法调用栈中或者特定实现中的某个内存区域中
7.2 -- JVM工作机制
下面简单分析一下JVM是如何执行字节码命令的,也就是说执行引擎是如何工作的.
7.2.1 机器如何执行代码
普通的实体机上程序是如何执行的 ?
前面已经分析了计算机只接受机器指令 ,其他的高级语言首先必须经过编译器编译成机器指令才能被计算机正确执行. 所以从高级语言到机器语言之间必须要有个翻译过程. 我们知道机器语言一般都是和硬件平台密切相关的 , 而高级语言一般都是屏蔽所有底层的硬件甚至包括软件平台(如操作系统). 高级语言之所以能屏蔽这些底层硬件架构的差异,就是因为有中间的一个转换环节 , 这个环节就是编译 ,与硬件耦合的麻烦就交给了编译器 ,所以不同的硬件平台通常需要的编译器也是不同的. 其实说不同的硬件平台差异不如说操作系统之间的差异 ,因为现在的操作系统几乎完全向用户屏蔽了硬件.
通常一个程序从编写到执行会经历一下一些阶段 :
源代码 - 预处理器 - 编译器 - 汇编程序 - 目标代码 - 链接器 - 可执行程序 , 源代码和最后的可执行程序中间的所有环节都是由现代意义上的编译器统一完成的 . 例如在Linux平台下我们安装一个软件通常需要经过 configure, make ,make install ,make clean 这4个步骤来完成.
configure 为这个程序在当前的操作系统环境下选择合适的编译器来编译,也就是选择合适的编译器和一些环境参数.
make 将源代码编译成目标文件
make install 将以编译好的可执行文件安装到操作系统指定或者默认的安装目录下
make clean 用于删除编译时临时产生的目录或文件
值得注意的是 , 我们通常所说的编译器都是将某种高级语言直接编译成可执行的目标机器语言( 实际上在某种操作系统中是需要动态链接的目标二进制文件 : 在windows下是 dll ,在linux下是 so库) . 但是实际上还有一些编译器是将一种高级语言翻译成另一种高级语言 ,或者将低级语言编译成高级语言( 反编译 ) ,或者将高级语言编译成虚拟机目标语言 , 如java编译器等.
再回到如何让机器( 不管是实体机还是虚拟机 ) 执行代码的主题 , 不管是何种指令集都只有几种最基本的元素 : 加减乘除, 求余求模等 .这些运算又可以进一步分解成二进制位运算 : 与 , 或 , 异或等. 这些运算又通过指令来完成 , 而指令的核心目的就是确定需要的运算的种类 ( 操作码 ) 和运算需要的数据 ( 操作数 ) ,以及从哪里获取操作数 , 将运算结果存放到什么地方 ( 寄存器或是栈 ) 等. 这种不同的操作方式又将指令划分为 : 一地址指令 , 二地址指令 , 三地址指令 和零地址指令等 n地址指令 .相应的这些指令集就会有对应的架构实现 . 如基于寄存器的架构实现或者基于栈的架构实现 , 这里的基于寄存器或者栈都是指一个指令中的操作数是如何存取的.
7.2.2 JVM为何选择基于栈的架构
JVM 执行字节码指令是基于栈的架构 , 也就是所有的操作数必须先入栈 , 然后根据指令中的操作码选择从栈顶弹出若干个元素进行计算后再将结果压入栈中. 这样一个简单的加法操作都需要频繁的入栈和出栈共5次 , 这和一般的基于寄存器的操作有所不同 . 如果是基于寄存器一般只需要将两个操作数存入寄存器进行加法运算后再将结果存入其中一个寄存器即可 . 那么为什么JVM还要基于栈来设计呢 ?
一. JVM 要设计成平台无关的 , 而平台无关性就要保证在没有或者有很少的寄存器的机器上也要同样能正确的执行Java代码 . 基于寄存器的架构很难做到通用 .
二. 为JVM更好地优化代码 .
三. 为了指令的紧凑性 . 因为Java的字节码可能在网络上传输 , 所以class文件的大小也是设计JVM字节码指令的一个重要因素.
多余的解释看书吧 =. =
总之就是基于寄存器的架构 得到了性能 , 牺牲了跨平台的移植性 , 而基于栈的架构虽然需要频繁的入栈出栈 , 但是更容易实现平台无关性 , 并且栈的体积小?
7.2.3 执行引擎的架构设计
了解了Java以栈为架构的原因后 , 再详细看一下JVM是如何设计Java的执行部件的 . 如下图
每当创建一个新的线程时 , JVM会为这个线程创建一个Java栈 , 同时会为这个线程分配一个PC寄存器 ,并且这个PC寄存器会指向这个线程的第一行可执行代码 ,每当调用一个新方法时会在这个栈上创建一个新的栈帧数据结构 , 这个栈帧会保留这个方法的一些元信息 ,如这个方法中定义的局部变量 ,一些用来支持常量池的解析, 正常方法返回以及异常处理机制等.
JVM 在调用某些指令时可能需要使用到常量池中的一些常量 ,或者是获取常量代表的数据或者这个数据指向的实例化的对象 ,而这些信息都存储在所有线程共享的方法区和Java堆中 .
执行引擎的执行过程 : =. = 看书把
JVM 方法调用栈 : =. = 看书吧
JVM 的方法调用分为两种 : 一种是Java方法调用 , 一种是本地方法调用 . 本地方法调用由于各个虚拟机的实现不太相同 .
总结 :
本章主要介绍了JVM的体系结构 , 以及JVM的执行引擎执行JVM指令的过程 , 实际上JVM设计非常复杂 . 这里只是简单的介绍一下 .
扩展 :
什么是Java本地方法 ? https://www.cnblogs.com/langtianya/p/3459647.html