从计算机的视角看待JVM

在正式开始介绍实验框架之前,我们想先讲讲什么是计算机。

大家都很熟悉的,也是最经典的冯·诺伊曼结构将计算机分为了五个部分:

  • 控制单元

  • 运算单元

  • 存储

  • 输入

  • 输出

在我们的实验中,我们也会将JVM看成是一个计算机,因为我们可以把JVM的所有内容划分到对应的模块中。这也许是一个相当奇怪的视角,但是它对我们有很大的帮助。

如果你搜索“JVM执行流程”这样的关键词,你会找到很多的博客,会看到很多的图。但是往往当你阅读完全篇时,脑子里依旧一团乱麻。这是因为文字描述的知识点对你来说没有什么感觉,它不够make sense。而图中往往又是一些陌生的概念,你完全不知道它为什么要有这些东西,只能强行记忆。这个时候你一定觉得非常困惑。

我们不妨思考一下,最开始编写JVM的人,他们是怎么进行设计的?

首先,要确定JVM是用来做什么的?

JVM是用来运行程序的,这也就规定了JVM的输入——用户提供的程序。

程序即文件,即磁盘上的二进制流。你常常会听到可执行文件这个概念,对于JVM来说,class文件就是它的可执行文件。

接下来面临的问题是,怎么运行这个程序?

class文件是一个完全静态的概念,它提供了非常多的信息,例如常量池、父类、接口,也指明了需要做的事,例如某个方法的所有code。因为class文件的结构是规范且固定的,从理论上来说,只要按照一定的顺序来解析这个文件,JVM就能获得所有完成程序语义的所需要的内容。

这个过程中,JVM去获取信息,在最原始的做法是它每一次都在需要某个信息时就去从头到尾读一遍class文件。但是不记录在哪一个位置读到了哪个信息,于是,下一次它需要同样信息的时候就需要再次读取。这样显然效率非常低。

因此,JVM会选择只对class文件进行一次解析,在过程中它把每一个位置上有什么信息都记录在自身的一个内部结构中。相当于做了一次缓存,这样它下次需要某个信息时直接读取这个内部结构就能获取到信息了。

现在,JVM已经把外部输入转化为内部信息了,然后呢?

既然class文件中指明了要做哪些事情,JVM就得找到一个方法帮它去完成。看看我们还缺什么?

class文件中指明要做的事情,其实就是方法执行。再具体一点,就是指令序列的执行。在源码被编译之后,无论是显式的方法调用,还是像声明局部变量、进行运算这样的操作,它们都是以指令的方式被记录在class文件中的某一部分的。

于是JVM把问题转换为了怎么样执行指令序列。

我们该如何理解指令序列?

我们在之前不断地提到了编译这个概念,源码经过编译这一步骤变成了class文件,那么编译到底做了什么?编译简单来说就是将Java语言编写的程序转化成了JVM认识的语言,而JVM的认识的语言就是它的指令集。

我们知道,复杂的事情往往可以拆分成很多个简单的事情的集合。比如,要完成两个数的乘法,你可以使用乘法的运算法则,当然也可以使用非常多次的加法甚至是位运算,这两者最终能得到的结果是一样的,它们的差异只在于过程的复杂度。

同理,JVM可以对外宣称,“我反正只支持位运算,编译器你不要乱来”,那么编译器只能老老实实地把所有的复杂的事情都转化成位运算。众所周知,JVM最后还是会把指令转换成实际操作系统上的代码,如果真的只有位运算,那么在CPU里实际跑的也就只有位运算指令。

CPU听了想打人。

被CPU打了一顿的JVM终于老实了,于是它决定重新做机提供更多的指令给编译器,这就是JVM的指令集。

指令集里的指令是怎么执行起来的?

要执行指令,JVM还需要解决很多问题。如果把一条指令当作一个算法,那么算法需要输入,也需要有一些数据结构提供给算法来在它上面进行操作。最后,算法也许还会产生一些输出,JVM如果想要使用这些输出,JVM也需要一些地方来存放它们。

因此,JVM需要有存储的结构,无论是用来做临时的计算还是最后放置信息。当然了,这个问题在之前将class文件转化为内部信息的时候其实就已经遇到了。

然而,JVM现在有了很多东西要记录,比如class文件转化后的信息,比如执行的中间结果。如果把它们都放在一起是不是非常混乱?当信息越来越多的时候就完全无法一下子找到自己想要的东西了。

出于这个需求,JVM又将操作系统分配给他的空间根据不同的功能重新分配了一下,这些空间就被称为了线程、内存...而因为数据来源或类型的不同,又进行了一些细分,例如线程中的操作数栈、局部变量表,内存中的堆、方法区。

有了存储的地方,接下来就应该能run起来了吧?

确实,现在只需要找个苦力来维持把所有的指令序列按要求执行就可以run起来了。在JVM中,这个苦力叫做解释器。解释器要做的事情很固定——拿到指令,看看指令要什么就提供什么,等指令运行结束,决定下一条要执行的指令是什么。

当然,在实际的实现中,这个固定的流程并不一定都是解释器完成的,这里说的解释器也只是概念,它和指令序列结合在一起对应了控制单元运算单元。

在指令序列执行完之后,JVM就完成了它的工作。根据指令的不同,也许它会对JVM的外部产生一定的影响,例如在控制台打印出了字符,例如修改了文件中的某个文件中的值,这些所有的对JVM的外界产生副作用就是JVM的输出

(我知道你们有些人会太长不看直接翻到这里,但是不看你是绝对理解不了我接下来要讲的东西的 👻 )

至此,我们对如何实现一个JVM已经有了初步的概念——JVM是一个计算机,它要完成工作同样需要输入、存储、控制、运算和输出这五部分的内容。从这个角度出发,就很容易理解JVM的各种模块,对这些模块的划分方式无非两种。一种是为了实现这五部分结构的基础模块,例如内存、线程。另一种是在已经有了基础模块之后,为了JVM更好的执行而设计出来的,例如即时编译JIT、虚函数表Vtable以及保证了并发中安全性的各种锁。