前言

很高兴遇见你~ 欢迎阅读我的文章。

JVM是每个Java程序员必须迈过去的一个坎,因为它实在是太重要了。Java的底层知识,归根结底,都是JVM相关知识。很多读者看到jvm就感觉:哦这是底层知识,算了先学好应用层再说。或者到了面试需要不得不背诵几道题。在我看来,了解JVM是理解整个Java生态的必经之路。编程语言的发展,从机器码01串到现在的高级Java语言,这里面凝结了无数先驱的智慧。了解JVM,不只是为了面试,更是为了感受前人的智慧,学习JVM也可以让我们的Java程序更加地健壮。

而关于非JVM研究人员来说,可能我们并需要很深入去研究,理解他的概念模型就已经满足了。作为一个Android工程师,JVM也是必学的内容之一。JVM基础系列文章,主要讲述关于JVM需要了解的关键基础知识,不会去深入研究JVM的底层知识。对于一个Android工程师,这些肯定是足够的了,对JVM有兴趣的读者,可以看完之后继续深入去研究,相信这一系列文章也可以很好地给你提供一个学习JVM的指引。

第一篇我们来聊聊JVM。为什么说JVM是人类在编程语言上迈出的一大步?JVM在整个Java程序运行中扮演了什么角色?JVM是用C语言编写的,那么C语言是不是属于跨平台语言?如果C语言是跨平台语言,为什么还要开发Java呢,C不是更快吗?为什么Java这么火,但C语言还是没有没落?带着这些问题,这篇文章一起来聊聊计算机语言的发展简史,看看机器码是如何一步步演变成Java的,了解这些知识,可以更好地认识JVM。

C语言的诞生

我们知道,计算机硬件他只认识01串机器码,计算机发展早期的时候,程序员是通过编写01串来写程序的,例如针孔卡带就是其中的一种形式:

当然那时候的计算机和我们当今的计算机是不可同日而语,那时的计算机只能实现一些很简单的操作。例如我们需要计算:4+8,那么计算的指令需要有:0100,1000,1101,其中第一二进制串是代表数字4,第二个是代表数字8,第三个代表加法指令(当然这些指令都是不存在的,只是拿来举例)。这种编写程序的方式不仅效率低下,复杂,代码语义不清晰,不适应于越来越复杂的程序开发。那么先驱们想到了一个办法,可不可以把这些指令都取一个“名字”,例如1101,写成add,然后再用一个编译器,把add映射成1101,这样下次写4+8,可以写成4,8,add,编译器可以根据我们的设定转换成0100,1000,1101,这样不就方便非常多。这,就是汇编语言的诞生由来。

正如上述所言,我们不仅可以把加法操作写成指令,还可以把减法、赋值、循环、分支等等写成指令,最后再编译成机器码即可。相比直接使用机器码编写程序,使用汇编语言编写程序,极大提高了开发效率,且适用于开发更加复杂的程序,代码的语义也更加清晰

不同机器,他的指令是不一样的,例如机器A的加法指令是0010,而机器B则是0101。汇编语言和cpu的机器码是一一对应的关系,因而不同的机器,具体的汇编语言也不同。随着计算机的发展,业务场景越来也复杂,汇编语言也开始显得心有余而力不足。这时候需要比汇编更高级的程序语言,来提高程序员的开发效率。这时候C语言诞生了。(当然中间还有一个B语言,以及其他的一些细节,这里主要讲语言的大体演变,不深究细节)。

和机器码形成汇编语言类似,C语言也对汇编语言进行了更高级的抽象。通过C语言编译器,就可以把C语言编译成汇编语言了。这样,工作效率再一次提高,完成更加复杂的业务逻辑。如下图:

前面我们讲到,不同平台的汇编语言是不同的,因而在不同的平台,需要编写不同的C语言编译器来把C语言编译成对应的汇编语言。

所以,一个C语言程序,要在一个机器上运行,首先要使用C语言编译器把C语言编译成该机器的汇编语言,然后再使用该机器的汇编语言编译器把汇编语言编译成机器码,这样这个c程序,才能运行。

先有鸡还是先有蛋?

我们知道C语言编译器大都是使用C语言写的,那么世界上第一个C语言编译器是用什么写的?先有鸡还是先有蛋….

其实C语言和C语言编译器,并不是一蹴而就变成今天这么完善的。早期的C语言编译器,是使用汇编语言编写的,但他只实现了C语言最基本的功能,例如基本数据类型、基本操作和基本代码控制。这样编译器的任务就比较少了,使用汇编语言可以比较快实现。然后,建立在已经实现的C语言基础上,使用c语言继续开发C语言特性,不断丰富编译器的功能。因而,才有了C0,C1,C2…各种版本的C语言,版本不断迭代,C语言的特性也越来越多,越来越好用。而C语言的进步,也促进了自身的发展,相当于一个良性循环。

和汇编语言发展到C语言类似,我们也可以在C语言的基础上继续发展更高级的语言,来满足我们的开发,如Java等。所谓0生1,1生C,C生万物。为什么计算机专业的学生,普遍都需要先学C语言?因为C语言,是当今一切高级程序语言的鼻祖,也是根基。有兴趣的读者可以阅读这篇文章深度解析:既然C编译器是C语言写的,那第一个C编译器是怎样来的?了解更多。

C语言是否可以跨平台?

我们在window操作系统上编写一个C语言程序,然后使用编译器,点击run,然后程序就跑起来了。我们把代码复制,然后在Linux系统上也进行编译,同样也可以运行。那么这是不是就证明,C语言在不同的平台是可以兼容的,也就是C语言是跨平台的?

答案是:no!首先,我们要明确跨平台这个概念。跨平台指的是语言即不依赖于操作系统,也不依赖硬件环境,一个操作系统下开发的应用,放到另一个操作系统下依然可以运行。C语言可以在不同平台编译运行的关键是:编译器,我们必须为C语言不同的平台编写编译器,然后再使用该编译器把C语言编译成对应的机器码,这个C语言程序才可以运行,他做不到平台无关性,因而C语言不是一个跨平台语言

那么只需为不同的平台写一个C语言编译器,同一份C语言程序,就可以在不同的平台运行了吗?答案是:NO!前面我们知道,C语言其实是和具体平台的汇编语言相关的,而汇编语言是和具体机器的机器指令相关,所以在不同的平台,C语言也会有所不同。例如整型变量int,在不同机器上可能为16位长或者32位长。有兴趣的读者可以阅读这篇文章C为什么不能跨平台了解更多。

跨平台主角:JVM

前面讲到,C语言是无法进行跨平台的,每个C语言程序都必须通过编译器编译成对应机器的机器码才可以运行。为了实现跨平台,我们需要有一个程序A,可以在运行时把代码动态编译成具体平台的机器码,这样同一份程序,就可以在不同的平台直接运行了,而不需要先编译成对应平台的程序。这个程序A就是虚拟机。这里的虚拟机并不是指Java虚拟机,而是泛指虚拟机,JVM只是虚拟机的一种。虚拟机要完成的事情是:一个程序不需要编译,可以直接运行该程序。虚拟机可以把我们写的程序当成自身的执行指令,然后一行行执行,而不需要全部转化成对应平台的机器码。

JVM,全名是Java虚拟机(Java Virtual Machine),但是他并不是支持Java语言,事实上JVM只认识class文件。我们编写的Java程序,需要进行编译,之后才能运行在虚拟机上。Java文件编译后的结果称为字节码,也就是class文件。class文件是虚拟机唯一认识的代码,就像机器只认识机器码一般,所以class文件也可以称为虚拟机的“机器码”。如下图:

JVM本质上是一个使用C语言编写的程序。这个程序有着和C语言编译器很像的功能:把高级语言编译成对应平台的机器码。但不同的是,C语言编译器必须把C程序完全编译之后再运行,而JVM可以直接运行class文件,边执行边编译为机器码。每个平台都有不同的虚拟机,和我们上面讲的不同平台有不同的C语言编译器的原理是一样的。一个平台想要运行Java程序,首先要配置Java环境,也就是安装JRE(Java Runtime Environment),Java运行时环境,可以认为是在该平台安装了一个JVM程序。当我们在一个平台运行class文件程序的时候,先启动虚拟机,虚拟机加载class文件然后运行。因此,我们编写的Java程序编译成统一的class文件后,只有该平台安装了虚拟机,就可以直接运行我们的程序,从而实现了跨平台。

所以,到现在看来,虚拟机确实很像一台“机器”,只是他的机器码并不是01串,而是class文件。JVM内部有着自己的内存划分,如方法栈、堆区、常量池等等。在实际的运行中,虚拟机也是把class文件编译成了机器码在真正的机器上运行。通过虚拟机,向我们屏蔽了真正的机器,而看到一台虚拟的机器。我们的开发也不再需要关心具体的机器如何,只需要关注JVM即可。

到这里不知道读者是否会好奇:为什么虚拟机不直接运行Java程序,而要先把Java编译成字节码(也就是class文件)才能运行呢?不是多此一举吗?这其实是JVM的另一个设计目标:语言无关性。上面讲到,JVM的存在,使得在各大平台可以直接执行class文件,也就是字节码,实现了平台无关性。而任何一个高级程序语言,只要能编译成class文件,那么这个语言就可以运行在JVM上,从而实现JVM的平台无关性。例如Groovy、kotlin,他们都可以编译成class文件,那么他们也就可以运行在JVM上,而这些语言,也可以统一称之为:JVM语言。如下图:

所以,更准确的来说,不应该叫做Java虚拟机,而应该是class虚拟机才对。

c语言为什么没有没落?

在Java语言等跨平台语言如火如荼的今天,为什么C语言这类编译型语言,还没有没落呢?反而常年占据编程语言排行榜的首位?这就涉及到虚拟机需要付出的两个重要代价:速度、环境

首先看第一个因素。虚拟机直接执行中间码,这看起来很美好,但实际上他需要先把中间码解释为机器码之后,才能真正运行。例如JVM的中间码则为class文件。那么运行的过程中就涉及到解释中间码的这个过程,而可执行程序本身就是机器码,他可以直接被执行,两者之间速度就相差非常多了。为什么ios系统总是感觉比android更流畅、更快,虚拟机就是一个非常重要的因素。IOS程序直接编译成可执行程序,速度快的同时,也付出了无法跨平台的代价。但,ios程序难道可以运行在别的系统上?事实上我认为谷歌选择JVM语言作为android的开发语言并不是一个最好的选择。我们开发的android程序,其实用不到跨平台这个属性。我们的程序,只会运行在android系统上,而不会运行在其他的系统,事实上也无法运行在其他的系统上。我们的程序需要调用到android系统的api,需要与系统交互,这本身就已经决定了我们必须运行在android系统上。选择JVM语言来开发android,带来了一个无关紧要的跨平台特性,却付出了巨大的性能代价。

第二个因素,环境。JVM语言需要JVM才能运行,一个平台必须安装了JRE之后,才能执行程序。那么对于一些小型的机器,如手表等,内存非常少,cpu能力也非常有限。JVM带来的内存代价和性能消耗是这些机器无法承受的。那么这个时候,C语言这一类的编译型语言就是最好的选择了。

这里还需要补充JVM的另外一个代价:JVM代码无法直接操作内存。一门语言越高级、越抽象,他和机器之间的相关性就越低。与人走得越近,和机器就会离得更远。C/C++这一类的语言,他们拥有非常强大的能力:操作内存。他们归根结底,也是从机器码演化过来的,所以他们非常适合在一些嵌入式或者需要操作机器内存的程序中,例如JVM。而Java等高级语言就显得无能为力。当然也有使用Java程序编写的JVM,但其实跟我们之前讨论的“先有鸡还是先有蛋”的道理是一样的,还是需要C语言的帮助。

总之,跨平台语言,和直接编译型语言,他们适用的场景是不同的。打败C语言的绝不是一个跨平台语言,而是另一个更加优秀的编译型语言,但目前来说,近期是不能出现的。

虚拟机家族

上面的内容更加偏向于广义上的虚拟机,这部分专门来聊聊Java虚拟机。

前面讲到虚拟机不止有一种,事实上,Java虚拟机也不止有一种。目前主流的虚拟机,也就是Sun/OracleJDK和OpenJDK使用的虚拟机,是HotSpot。HotSpot最初由一家小公司开发,后来被Sun公司收购并进行研发。Oracle收购Sun之后,把BEA的JRockit虚拟机的优秀特性整合到了HotSpot上。同时,由于Sun/OracleJDK在Java应用中处于统治地位,HotSpot也称为使用最广泛的虚拟机。此外还有前面提到的BEA公司的JRockit虚拟机、IBM的J9虚拟机等等。前者被Oracle收购并整合到HotSpot中,后者目前仍然很活跃,但和HotSpot相比还是显得比较小众。

Android执行的是Java程序,自然也有虚拟机。不过Android虚拟机并不能称为JVM,因为他没有满足Java虚拟机规范,而是根据移动端的特点进行了改造。他是基于寄存器架构而不是栈架构,同时Android虚拟机运行的不是class文件也不是jar文件,而是dex文件,但dex文件可以从前两者转化而来。最开始Android的虚拟机是Dalvik虚拟机,为了提高性能,使用了JIT编译器(Just In Time Compiler)来编译字节码。JIT的特点是:把使用过程中的热点代码,也就是使用比较频繁的代码先解释为机器码,这样,下次运行到这个地方的时候,就可以提高速度。也就是运行时间越久,应用越流畅。到了android5.0之后,Dalvik虚拟机就替换成了ART虚拟机。ART的特点是AOT(ahead of time comlilation),也就是提前编译。他可以在安装应用的时候,就把代码全部编译成机器码,这样整体的性能就得到了非常好的改善。但却付出了不少的代价:安装时间长、需要一定的空间来存放机器码。而后,在Andoid7.0,就把JIT也加入了ART虚拟机,这样在安装的时候,只需要先提前编译一小部分,等到运行的时候,遇到还没有编译的地方,再使用JIT进行编译。

总结

本文讲解了程序语言的发展历程,以及跨平台的实现关键:虚拟机,最后再讨论了跨平台语言与编译型语言的特点以及虚拟机家族。

虚拟机作为跨平台语言的关键,使我们学习跨平台语言不可忽视的知识,学习虚拟机之前,先了解虚拟机的背景以及虚拟机解决的问题是必要的。我们今天得以使用Java开发出各种复杂的应用,也是因为开发虚拟机这种粗活累活,有前辈帮我们做了,现在也还在不断地去完善。这是几十年来千万前辈的智慧结晶,秉着对知识的敬畏,更加值得我们去学习。

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。
笔者能力有限,有任何想法欢迎评论区交流指正。
如需转载请私信交流。

另外欢迎光临笔者的个人博客:传送门