JVM体系结构与工作方式

计算机指令集与程序关系

所谓指令集就是CPU中用来计算和控制计算机系统的一套指令合集,每一种新型的CPU在设计时都指定了一系列与其他硬件电路相配合的指令系统。而指令集的先进与否也关系到CPU的性能发挥,它是体现CPU性能的一个重要指标

CPU支持的指令集可以通过CPU-Z软件查看到

从主流体系指令集可以分为精简指令集(RISC)和复杂指令集(CISC),而我们普遍使用的桌面操作系统总基本上使用的都是CISC, 如x86架构的CPU都是用复杂指令集

指令集与汇编语言有什么关系?指令集是可以直接被机器识别的机器码,也就是它必须以二进制格式存在计算机中。而汇编语言是人能够识别的指令,汇编语言理论上与机器指令集一一对应,换句话说,汇编语言是为了让人们能够更容易地记住机器指令而使用的注记词

指令集和CPU架构有何关系?如Inter和AMD的CPU指令集是否兼容,也就是CPU体系是否会影响指令集?答案是肯定的。不同的芯片架构设计会对应到不同的机器指令集合,但是现在不同芯片厂商往往都会采用兼容的方式来兼容其他厂商其他不同架构的指令集。这是因为现在操作系统才是管理计算机的真正入口,几乎所有的程序都是通过操作系统来调用的,所以操作系统不支持某种芯片的指令集,用户的程序是不可能运行的。如果没有操作系统和应用程序,再好的CPU也没有使用价值

JVM(Java Virtual Machine)Java虚拟机通过模拟一个计算机来达到计算机所具有的计算能力。JVM和实体机一样必须有合适的指令集,这套指令集能够被JVM说解析执行,这个指令集就是JVM字节码指令集了,符合class文件规范字节码都可以被JVM执行

JVM体系结构

JVM结构基本上由4个部分组成:

  • 类加载器,在JVM启动时或者在类的运行时将需要的class加载到JVM中
  • 执行引擎,执行引擎的任务是负责执行class文件中所包含的字节码中,相当于实际机器上的CPU
  • 内存区,将内存划分为若干个区以模拟实际机器上的存储,记录和调度功能模块
  • 本地方法调用,调用C/C++实现的本地方法代码返回结果

1.类加载器

每个被JVM装载的类型都有一个对应的java.lang.Class类的实例来表示该类型,该实例可以唯一标识被JVM装载的class类,要求这个实例和其他的实例一样都存放在Java的堆中

2.执行引擎

执行引擎是JVM的核心部分,执行引擎的作用就是解析JVM字节码指令,得到执行结果。Java虚拟机规范只制定规范,而具体的执行引擎实现由JVM的实现厂商自己去实现。如SUN的hotspot是基于栈的执行引擎,而Google的Dalvik(Android)是基于寄存器的执行引擎

每一个Java线程就是一个执行引擎实例,在一个Java进程中就会同时有多个执行引擎在工作,这些执行引擎有的在执行用户的程序,有的在执行JVM内部的程序(如Java垃圾回收器)

3.Java内存管理

一个JVM实例(Java应用进程)都会有一个方法区,Java堆,Java栈,PC寄存器(程序计数器)和本地方法区。其中方法区和Java堆是所有线程共享的,也就是可以被所有的执行引擎实例访问到。每个新的执行引擎实例被创建时都会为这个执行引擎创建一个Java栈和程序计数器,如果当前正在执行一个Java方法,那么在当前的Java栈中保存的就是该线程中方法调用的状态,包括方法的参数,方法的局部变量,方法的返回值以及运算中的中间结果等。而程序计数器会指向将执行的下一条指令

如果是本地方法调用,则存储在本地方法调用栈中或者本地方法区内

机器如何执行代码

计算机只接受机器指令集,其他高级语言首先必须经过编译器编译成机器指令才能被计算机正确执行,所以从高级语言到机器语言之间必须要有个翻译过程。高级语言之所以能够屏蔽底层硬件架构的差异就是因为中间有一个转换环节,这个环节就是编译,与硬件耦合的麻烦就交给了编译器,所有不同硬件平台通常需要的编译器也是不同的。实际上我们这里说的不同的硬件平台差异已经被操作系统替代,与其说不同操作系统的差异还不如说操作系统之间的差异,因为现在操作系统几乎完全向用户屏蔽了硬件,编译器与操作系统关系非常密切

我们通常所说的编译器都是将某种高级语言直接编译成可执行的目标机器语言(实际上在某种具体的操作系统中是需要动态链接目标二进制文件: 在windows下是dynamic link library,DLL;在Linux下是Shared Library, SO库),但是还有一些编译器是将一种高级语言编译成另一种高级语言,或者将低级语言编译成高级语言(反编译),而Java编译器则是将高级语言编译成虚拟机目标语言(字节码)

不管何种指令集都只有几种最基本的元素:加,减,乘,除,求模等。这些运算又可以进一步分解成二进制位运算:与,或,异或等。而这些运算又通过指令来完成,而指令的核心目标就是确定需要运算的种类(操作码)和运算需要的数据(操作数),以及从哪里(寄存器或栈)获取操作数,将运算结果存放在什么地方(寄存器或栈)等

这里基于寄存器或者栈都是指在一个指令中的操作数如何存取的问题

JVM为何选择基于栈的架构

JVM执行字节码指令是基于栈的架构,也就是所有的操作数必须先入栈,然后根据指令中操作码选择从栈顶弹出若干个元素进行计算后再将结果压入栈中。每个方法调用时就会给这个方法分配一个本地变量集,这个本地变量集在编译时就已经确定,所有操作数入账可以直接是常量入栈或者从本地变量集中取一个变量压入栈中。一个操作需要频繁的入栈和出栈,如进行一个加法运算,如果两个操作数都在本地变量集中,那么一个加法操作就需要5次栈操作,分别是将两个操作数从本地变量入栈(2次入栈操作),再将两个操作数用于加法运算(2次出栈),再将加法结果压入栈顶(1次入栈)。如此频繁移动数据的操作,为什么JVM还要基于栈来设计呢?

主要有如下几个理由:

  • JVM要设计成与平台无关的,而平台无关性就要保证没有或有很少的寄存器上也能同样正确地执行Java代码,基于寄存器的架构很难做到通用。Google的Android平台上的Dalvik VM就是基于特定芯片(ARM)设计的基于寄存器的架构,这样的在特定芯片上实现基于寄存器的架构可能是更多考虑性能,但是也牺牲了跨平台的移植性,当然手机上跨平台的需求不是最急迫的
  • 还有一个理由是为了指令的紧凑性,因为Java的字节码可能在网络上传输,所以class文件的大小也是设计JVM字节码的一个中要因素。尽量让编译后的class文件更紧凑,提高自己吗在网络上的传输效率

总结

每当创建一个新的线程时,JVM会为这个线程创建一个独立的Java栈,同时分配一个程序计数器,这个程序计数器指向这个线程的第一行可执行代码,对应还有一个执行引擎将会执行代码。但这个线程每调用一个新方法就会将这个方法在这个栈上创建一个新的栈帧数据结构(入栈),这个栈帧会保留这个方法的一些元信息,如在这个方法中定义的局部变量表,一些用于支持常量池的解析,正常方法返回以及异常处理机制等

实际上JVM在执行字节码时还会优化这些字节码,比如将它们再编译成本地代码,也就是JIT(Just In Time及时编译)技术,这个技术在测试时可能会带来干扰,如果程序没有充分"预热",那么测试得出的性能结果就会不准确,例如,JVM会记录某个方法的执行次数,如果执行的次数到达一个阙值(客户端一般是1500次,服务器一般是10000次)时JIT就会编译这个方法为本地代码

posted @ 2020-11-29 16:59  OverZeal  阅读(152)  评论(0编辑  收藏  举报