什么是虚拟机?“虚拟机”是一个很大的概念。从字面上理解,“virtualmachine”就是“虚拟计算机”。我们在学习服务器端编程的时候,相信大部分同学都接触过虚拟机。有这样一个场景,由于我们日常使用的电脑大部分都是Windows操作系统,但是大部分服务器软件运行在Linux系统上,假设我们在Windows上编程,我们不能直接在Windows上编程。测试起来很不方便。基于这样的场景,就有了一个虚拟机,可以在windows系统的基础上运行linux系统,然后我们就可以很方便的在windows系统上测试linux系统的程序了。这个Linux操作系统是通过一定的技术手段虚拟出来的,中间过程非常复杂,不是三言两语可以描述的。今天要说的虚拟机和上面说的虚拟机略有不同,但是它们要解决的问题是一样的。上面说的虚拟机虚拟出一个完整的操作系统,我称之为“操作系统级虚拟机”。我们今天要说的虚拟机是针对编程语言的。它能达到的效果是相同的代码在不同的操作系统上运行,输出相同的结果。它可以一次编写,到处运行。我称之为“语言级虚拟机”。我们非常熟悉的Java、PHP、Python等编程语言,其实都是基于虚拟机的语言。它们都是跨平台的。我们只需要编写一次代码就可以在不同的操作系统上运行,并且输出完全相同的结果。学过系统编程的同学应该都知道,不同的操作系统为同一个功能提供的“系统API”可能是不一样的。比如Windows和Linux系统都提供了网络监控API,但是它们对应的SOCKETAPI是不一样的。假设我们使用与平台相关的编程语言(如:C、C++),那么在编程时就必须注意这样的差异。并且针对不同的操作系统做相应的兼容处理,否则程序在Linux系统上可以正常运行,但是Windows会报错。类似的差异还有很多,具体细节以相应的系统编程手册为准。有的系统API完全不同,有的只是个别参数不同,方法名完全一样。程序员在写代码的时候需要注意这些,才能写出健壮的跨平台代码,这对新手来说是非常困难的,这样一来,程序员就需要把很大一部分精力花在兼容性问题上,而不是专注于关于实际功能的开发。有了虚拟机,以上问题就不存在了。简单的说,虚拟机的作用就是一个中介代理。比如我们刚来大城市想租房子,北京、上海、广州等大城市的房东实在是太多了,如果没有房产中介(虚拟机),我们就需要联系N个房东,才能租到合适的房子;有了房产中介(虚拟机),我们只需要告诉房产中介(虚拟机)我们想租什么样的房子,房产中介(虚拟机)就会协调各个房东,我们就可以租对房子,过程不同,但最终结果是一样的。同理,以SocketAPI调用为例,我们把写好的代码交给虚拟机,然后由虚拟机负责调用系统API,相当于中间加了一层中介代理,虚拟机会根据操作系统选择正确的。SoekctAPI,帮助我们完成最终的功能。这样做的好处是程序员无需再关注底层API的细节,可以专注于编写真正的功能。虚拟机帮助我们屏蔽了底层系统API的细节,大大降低了编程的门槛,代码的健壮性也大大提高。PHP执行过程PHP解释执行过程了解PHP的同学都知道,PHP是一种解释型语言,也称脚本语言,特点是轻量级、易用。传统的编程语言需要经过编译链接,才能执行并输出结果。脚本语言(PHP)省略了这个过程,直接通过shell命令执行并输出相应的结果,非常轻量、直观、易用。说实话,我入坑编程的时候也是学了Java的。为什么我最后还是入了PHP的坑?可能是这些特点吸引了我。刚才我们只讲了PHP的优点,但是很多时候都是有得有失。我认为编程语言也是如此。PHP非常轻巧易用,所以一定要牺牲一些优点,否则为什么呢?其他编程语言不会这样做。接下来说一下PHP的执行过程。想了解PHP的执行过程,从而理解PHP语言设计的选择。下面是PHP中启用Opcache缓存后程序运行的主要过程。图-1从图-1可以看出,PHP代码文件加载后,通过词法分析器(re2c/lex)从代码中提取单词符号(token),再通过语法分析器(yacc/bison)),从token发现语法结构后,生成抽象语法树(AST),然后通过静态编译器生成Opcode,最后由解释器模拟机器指令执行每一个Opcode。另外,当PHP打开Opcache时,ZendVM会缓存Opcode,缓存在共享内存中。不仅如此,ZendVM还会对编译后的Opcode进行优化。编译优化技术包括方法内联、常量传播、重复代码删除等。有了Opcache,不仅可以省略词法分析、语法分析、静态编译等步骤,而且Opcode也得到了优化,程序的执行效率大大提高比第一次执行更快。以上就是PHP解释执行的过程。虽然解释和执行对程序员很友好,省略了静态编译的步骤,但实际上这个过程并没有省略。只是虚拟机帮我们完成了,代价是牺牲了一部分性能。这带来了重量轻、易用性和灵活性。其中,词法分析、语法分析、静态编译、解释执行等过程都是在执行过程中完成的。编译型语言的执行过程了解了解释型语言的执行过程后,我们再来看一下编译型语言的执行过程作为对比,看看它与解释型语言有何不同。图2从图2可以看出,虚线框内的执行过程包括:词法分析、语法分析、编译。这三个步骤在PHP解释执行的时候也存在。唯一不同的是,C/C++的Step3是由编译器在编译过程中提前完成的,这样可以在运行时节省大量的时间和开销。汇编代码生成后,第四步就是链接汇编文件,生成可执行文件。这里的可执行文件指的是二进制机器码,不需要额外的翻译就可以直接被CPU执行。这四个步骤统称为静态编译。可以明显看出,与解释型语言相比,编译型语言需要在前期做更多的工作,但换来更高的性能和执行效率。因此,一般在大型项目中,由于性能要求比较高,代码量大,如果使用解释型语言,执行效率会大大降低,而静态编译型可以获得更好的执行效率,减少服务器采购成本。什么是准时制?JIT可以说是虚拟机中技术含量最高的技术。刚才我们分别讲了解释型语言和编译型语言的执行过程,也分析了它们各自的优缺点。我们可以考虑一下。没有一种技术既具有解释型语言轻量易用的优点,又具有编译型语言的高性能。结论是JIT。接下来我们要介绍的是编程语言中的JIT技术。它的全称是“即时编译”。它具体指的是什么?我们先看看维基百科对即时编译的定义。在计算机技术中,即时编译(英文:just-in-timecompilation,简称JIT;又译即时编译、实时编译),又称动态编译或运行时编译,是一种执行计算机代码的方法。一种方法涉及在程序执行期间(运行时)而不是在执行之前进行编译。通常,这涉及将源代码或更常见的字节码转换为机器代码,然后直接执行。实现JIT编译器的系统通常持续分析正在执行的代码并确定代码的某些部分在哪里编译或重新编译。刚才我们说了,JIT既有解释型语言的轻量级易用性,又具有高性能,那么它是如何实现的呢?以PHP8加入的JIT特性为例,下图描述了启用JIT特性后PHP的执行过程。PHP8-JIT是在Opcache优化的基础上更进一步,对保存在Opcache中的Opcode进行优化,然后进行编译。将Opcode编译成CPU可识别的可执行文件,即二进制文件,相当于C++编译出的可执行文件,但是这个过程在运行前不需要完成,而是在运行时,虚拟机启动一个后台线程,将Opcode转换成二进制文件。二进制文件缓存后,下次执行逻辑时,CPU可以直接执行,不需要再解释。理论性能与C++相同。这样做的好处是既保留了PHP语言的易用性和灵活性,又获得了高性能。图-3JIT触发条件JIT实际上是将运行时的一部分代码转换成可执行文件并缓存起来,以加快下一段代码的执行速度。那么程序启动后会不会触发JIT呢?JIT在程序第一次启动时不会工作。可以这样理解,PHP/Java代码在第一次执行时,还是以解释的形式运行。程序运行一段时间后需要触发JIT。说到这里,你是不是和我有同样的疑问,为什么JIT不把所有的代码都转成可执行文件,在程序启动的时候缓存起来,就像C++那样,效率不是更高吗?Java语言中确实有少量这样的应用,但不是主流。主要原因有:全部编译成二进制文件需要花费大量时间,程序启动会很慢,这对于大型项目来说是不能接受的。并非所有代码都需要针对性能进行优化。大部分代码其实在场景中用到的并不多。编译成二进制会占用很多容量。提前编译相当于静态编译。与静态编译相比,JIT编译有很多不可替代的优势。JIT触发条件主要是基于“计数器热点检测”,虚拟机为每个方法(或代码块)创建一个计数器,如果执行次数超过一定阈值,则认为是“热点方法”。之后达到阈值,虚拟机启动后台线程,将代码块编译成可执行文件缓存在内存中,以加速下次执行,以上只是对热点代码的触发规则的简单描述,采用的规则通过实际的虚拟机会比这更复杂。JIT和提前编译的优缺点JIT编译器是在运行时进行的,我们不难发现它与提前编译相比有几个明显的缺点。首先,JIT编译需要在运行时消耗计算资源。本来,这些资源可以用来执行程序。不管JIT编译器怎么优化(比如:分层编译),这都是无法回避的问题,而最耗资源的一步就是“过程间分析”,比如分析这个方法是否永远不能调用,是否抽象方法永远只会调用单一版本的结论,这些信息对于生成高质量代码非常有价值,但是要准确获取这些信息必须经过大量耗时的计算,消耗大量的资源运行时的计算资源。反之,如果提前编译完成这些耗时的工作,运行时只需要享受高质量代码带来的高性能,顶多提前编译稍微慢一点,但这是可以接受的。说了这么多,难道JIT编译相对于提前编译在性能优化上真的没有优势吗?结论是否定的,JIT编译有很多早期编译无法替代的优势。正是因为JIT编译器是在运行时进行的,所以JIT编译器才能获取到程序的真实数据。通过在程序运行时不断收集监控信息并分析这些数据,JIT编译器可以对程序做一些事情。积极的优化,这对于提前静态编译器是不可能的。第一,性能分析指导优化。比如JIT编译器在运行的时候,通过程序运行的监控数据,如果发现某些代码块执行的特别频繁,就可以重点优化这块代码,比如:分配更好的寄存器,缓存,等等这个代码。.然后是积极的预测优化。比如有一个接口,有3个实现类,但是在实际运行过程中,95%以上的时间都在运行实现类A。通过数据分析,可以进行激进的预测。每次执行A。如果多次发现预测错误,可以回到解释状态重新执行,但这只是小概率事件,不影响程序执行结果。最后,还有链接时优化。传统编译器的步骤是编译优化和链接是分开的。这意味着什么?添加一个程序需要用到A、B、C三个库,编译器首先对这三个库进行编译,通过各种方式进行优化,转换成汇编代码保存在一个文件中。最后一步就是将这三个库和程序集文件链接在一起,最后转化为可执行文件。这里有一个问题。A、B、C三个库在编译时分别进行了优化。无法假设A和B中的某些方法重复执行,或者可以通过方法内联来优化。但是JIT编译器不同的是,它在运行时是动态链接的,可以优化整个程序的调用栈,更加彻底。总结写这篇博客的主要目的是总结一下自己这段时间学习虚拟机相关技术的过程。在Google上搜索PHP虚拟机相关的文章时,发现可供参考的文章寥寥无几。由于Java和PHP的执行原理非常相似,我想通过学习Java虚拟机就可以理解ZendVM的工作原理了。Java虚拟机非常成熟,可以说是虚拟机的鼻祖。关于JVM世界有很多优秀的书籍。打开了我的新世界,让我对虚拟机有了新的认识,JIT技术更是让我惊叹。最后,PHP是世界上最好的语言!深入了解PHP操作码优化参考《深入理解Java虚拟机(第3版)》PHP8新特性JIT介绍深入了解PHPJITJava9AOT初探PHP的JustInTime编译器是如何工作的
