一、虚拟机的起源
1968 年 IBM 研究员 Robert A. Nelson、David Sayre 创造了 IBM M44/44X 实验性计算机系统。它对分页、虚拟机和计算机性能测量都作出了开创性贡献,但直到 Robert Creasy 领导的 IBM CSC 中心创造了 CP-40,才形成了当代虚拟机的雏形。
19 世纪 60 年代,分时(Time-sharing)系统已经发展得很成熟:在分时系统上,即使有多个任务同时运行,但在单个任务的视角就像“独占”所有资源。彼时,IBM 的研究员正在进行新一代数据处理系统研究,但他们只拥有一个 S/360-40 处理器,于是借鉴“分时”思想,实现多个 S/360 操作系统同时运行。这使“任务分时”更进一步,进入了“系统分时”时代。
IBM 构建了 CP-40 虚拟机管理程序(Hypervisor),并演进为 CP-67。CP-67 一经推出就大受欢迎。对于客户来说,买一台 S/360 机器,却能让十多个用户都拥有一台“个人电脑”,真是划算!
虚拟机的出现奠定了云计算的基础。单台服务器的资源通常会超过一个小应用的需求,让多个应用共享同一台服务器的资源才能让利用率最大化。服务器的基础电费也能让“应用们”一起分摊一下,就像买一件快递要 6 元运费,买满 99 就不需要运费了,直接包邮,多好。
虚拟机在云计算外也有大作用,至少有一半的开发者每天都和虚拟机亲密接触:Android 和 IOS 开发者在开发阶段通常都会在桌面系统运行一个模拟器调试,而不是链接真机。Java 和 JavaScript 开发者更不用说,只要程序在运行,就在和虚拟机打交道。
二、模拟器和虚拟机
可能有读者会感到疑惑?Java(JavaScript)程序运行时也没弹出一个“小手机”,怎么就是虚拟机了?这就要提到两个重要的概念:
- 系统虚拟机(System virtual machines):完整的虚拟机,可以替代硬件。
- 进程虚拟机(Process virtual machines):只用来执行与硬件平台无关的程序。
直白来说,系统虚拟机(模拟器)是 Minecraft(我的世界),虽然是假的,但确和现实世界有相同的运行规则。它是用软件模拟了真实的 CPU、内存、硬盘等。
进程虚拟机则不一样,它没模拟真实的硬件,而是用软件定义了一套新的软件,这个软件能接收特定的指令,并利用底层的硬件执行这些指令,而这些指令和特定的硬件无关。
不同体系结构的 CPU 提供了不同的指令集合(简称指令集),常见的有 X86、ARM、RISC-V。指令集间不能相互兼容,就像你没法给只懂英语的人要“三克油”,他会以为是"Thank you"。Java 有 HotSpot 虚拟机。JavaScript 有 V8 引擎,V8 现在已经内嵌到了绝大数浏览器中,可以说是使用最广泛的虚拟机。
进程虚拟机大大简化了程序员跨平台移植的工作,特别是客户端程序要在“千奇百怪”的设备上运行,有了它就基本不用关心硬件底层。
三、不用虚拟机也要跨平台
有些读者可能认为,一些语言(如 Go、Rust)不需要虚拟机也能“跨平台运行”。其实,这些语言本身并不能跨平台,而是由编译工具代劳,在编译时生成不同硬件平台的程序,即:交叉编译。
交叉编译不是一个新名词,在C/C++ 时代,交叉编译已经应用广泛,特别是在嵌入式开发,基本都是交叉编译。但是 C/C++ 语言本身的跨平台性做的不好,标准库和操作系统(UNIX/Windows)强耦合,也就没法直接生成各种平台的程序。Go、Rust 这类“次世代”语言,在刚开始作语言定义时,就考虑到了跨平台,基础类库能够做到操作系统无关,但这不是跨平台运行。
客户端对跨平台运行要求高,JavaScript 和 JVM(Android)的地位基本无法撼动。但服务端设备硬件基本统一,为啥 Java 仍占有一席之地?
这是因为 Java 在跨平台的同时,还实现了高性能(JavaScript 也一样)。这不得不提运行时编译技术 JIT(Just-In-Time Compiler),它实现了“边开车边换轮胎”。虚拟机有一套自己的“指令集”,用户程序的指令需要虚拟机“翻译”成实际硬件平台的指令,这样的“翻译”过程有性能损耗。Java 在 1.2 版本开始引入 JIT 技术,经常使用的代码(即热点代码,HotSpot)一边运行一边生成硬件的指令,以后运行这些代码就直接使用硬件指令,没有“翻译”的损耗。
而且,JIT 编译的代码质量更高。JIT 是运行了程序再编译,已经直到程序运行的结果。这就像是“开卷考试”,已经知道了答案了再写过程,当然可以更简洁、清晰,直达要害。