这里有三种执行方式,解释执行,即时编译(just in time),超前编译(ahead of time)
我们知道,机器能够执行的,是机器码,native code,也就是01组成的那些东西,但是我们写出来的源码,都是一些英语单词以及符号的拼凑,不管是什么编程语言。我们写的是英语单词以及符号的源代码,但是机器能够执行的是机器码
那要让机器执行我们写好的源代码,就需要把源代码翻译成机器能够执行的机器码,然后交给机器执行
这里有两个步骤,第一步翻译,也就是编译,第二步执行
所谓的解释执行,就是边编译,边执行,这个叫做解释器,interpreter,大部分脚本语言,都是解释执行,比如javascript,python,ruby,perl,groovy这些
还有一种,为了执行的时候速度快一点,就在编写源码之后,将源代码编译成机器码,这就是编译compilation,这样做之后,在执行的时候,因为已经编译好了,所以执行起来,性能就比较好,就快,传统上c等语言,都是这种方式
那后者在执行的时候,性能更好,但是多了一步,编译,前者因为编译和执行是在一个过程里面,所以执行的时候,需要先编译再执行,所以性能就差,所以脚本语言一般性能都比较差,所以多数时候,脚本都是用在一些性能不敏感的领域,比如web,而前者,则多用在一些性能敏感的领域,尤其是对latency延迟比较敏感的领域,比如游戏领域,还有苹果的应用商店,为了客户的体验,同时也为了打击热更新,所以原则上会要求开发者将app编译成native方能上架
然后我们来说java,你可能注意到了,我们一开始列举了三种编译方式,但是我们前面只说了两种,解释执行以及编译成机器码
那这里我们要说一下,编译成机器码的一个问题,那就是不同平台上用的机器码是不一样的,你在windows上用的机器码,就不能在macos以及linux上执行,如果同样的源代码,你想在另外一个平台上执行的话,那么你得到目标平台上,再做一次编译,比如你在windows上写好了源代码,但是你想把你写好的源代码放到macos上去执行,那么对不起,你需要再去找一台macos,然后在macos上编译之后,才能执行。当然有一种说法叫做交叉编译,就你可以在win上编译出mac上能够执行的程序,但多数时候,这个都难以令人满意,就你压根找不到交叉编译的工具,没有人去做这事
那为了写一个源码而不需要到处编译,所以java就提出了一个jit的概念,那就是,在编写完源代码之后,编译成一个字节码,bytecode,这个字节码,是介于源代码和机器码之间的一种过渡性质的东西,然后java会针对目标平台,提供不同的虚拟机,比如win上就提供win上的虚拟机,mac上就提供mac上的虚拟机,这个虚拟机在执行的时候,会将字节码,翻译成机器码,并最终执行
这样就把整个程序啊,变成跨平台和不跨平台的两个部分,不跨平台的部分就是java的虚拟机,跨平台的部分就是字节码(class文件,或者将class文件根据某种格式压缩打包的jar,jar其实就是zip,换个后缀而已)
那这样做的好处就是,一次编译,到处运行。我只需要编译出字节码,然后交给目标平台上的虚拟机去执行就可以了,我更新程序,并不需要再跑到不同的目标平台也就是操作系统上去编译,就方便了很多,而且编译成机器码的过程很慢,编译成字节码要快很多,java的编译速度是很快的,这个如果你有大型c或者c++项目的经验就知道了,编译成机器码很慢
同时,java的虚拟机,还会记录翻译字节码为机器码的过程,并做出优化处理,所以在运行一定时间之后,java的执行效率就会逼近甚至超过直接执行机器码的效率。为什么会超过呢?因为它会根据编译的过程提供的一些信息,做出一些的优化
那为了区别这两者,java的这种先编译成字节码,然后再交给虚拟机执行的过程,叫做jit,just in time compilation,即时编译,为了以示区分,把直接编译成机器码,再执行的过程,叫做aot,ahead of time compliation,超前编译
那这里说一下java提供的编译器,openjdk里面有jvm,也就是虚拟机,这个虚拟机的名字叫做hotspot,然后openjdk里面的编译器,叫做c1或c2,一般用后者,前者是针对客户端设计的
那现在java世界,有一个新的编译器,叫做graal[1],graal可以提供跟c2同样的功能,同时它也能提供aot编译器,也就是graal可以把java的源代码编译成机器码,而不仅仅是字节码
graal是用java写的,而openjdk是用c++写的
所以graal是用java实现了java源码的编译,这个就是编译器的自举
那jit和aot各有优劣,一个直观的对比如下:
可以看出来,aot在启动速度,内存使用,以及编译后的包的规模上,都有明显优势,而jit则在吞吐和减少最大延迟上,有优势,因为jit可以根据编译过程,自动做出优化,所以jit启动比较慢,然后执行的效率,需要在一定时间之后,才能逼近甚至超过机器码的执行效率,这个过程叫做warmup,热身
所以虚拟机是不是java的解释器,严格说起来,并不是,但是它的即时编译的模式,跟多数脚本所使用的解释执行,有一定的相似之处
一般认为,jit即时编译,是结合了解释执行以及aot超前编译两者优点的产物,当然这个并不完美,有代价,需要做取舍,如果你非常在乎启动时间,内存使用的大小或者编译后产物的大小的话,你可能还是倾向于用aot超前编译,而不是jit即时编译
当然我个人觉得,能够让用户自由选择jit还是aot,才是最吼的,就像java这样,谷歌的flutter/dart也做到了同时支持jit和aot,一般建议开发debug时候用jit,release发布时候再用aot处理。苹果的xcode将来应该也会支持jit,但是目前swift这些主要还是用aot,因为毕竟jit在debug时候,在编译速度上,优势明显
最后,java现在也支持直接执行.java文件,现在openjdk也可以解释执行了,这个强化是jep 330的东西[2],另外jdk现在也提供了jshell,也就是java的repl,所以大部分脚本的功能,其实jdk也都有了
所以现在的java,其实是同时支持:解释执行,即时编译(jit)以及 超前编译(aot)三种模式,用户可以根据需要,选择自己想用的方式,做或者不做编译,以及编译成什么东西