Java 的办法是编成谁也不认、只有虚拟机能认的中间码,各平台各装一份虚拟机——一次编译,到处运行。
7.4 说清了编译这件事,这一节看一种把编译产物做成【跨平台通用】的方案。
一、把问题倒过来想
之前的思路是:让代码去适配每一种机器——所以要为每个平台各编一份。 Java 的思路反过来:让机器来适配同一份代码。 具体做法是不再编成本机代码,而是编成一套自定义的中间码。 中间码不针对任何 CPU,所以它天然就是跨平台的——只要那台机器上有一个认识它的程序。
二、虚拟机就是那个【认识它的程序】
这个程序就叫虚拟机(JVM):各平台各装一份,负责把中间码执行掉。 于是链条变成了:源码 → 中间码 → 虚拟机 → 本机代码。 多出来的这一层,把【平台差异】从开发者手里,转移到了虚拟机厂商手里。 开发者的那句口号就是这么来的:一次编译,到处运行。
三、代价也明明白白
多了一层,就跑得慢一些。 虚拟机要一条条解释,或者运行时再编译。 要吃更多内存,虚拟机自己也要占地方。 目标平台上必须能装上虚拟机——装不上,这招就失效了。 所以现实里常常是混合的:热点代码由虚拟机在运行时编成本机代码,两头的好处都要一点。
四、这招其实到处都是
Python 也是虚拟机:源码先编成字节码,再由 Python 虚拟机执行。 各种脚本语言基本都是这个套路:中间码加解释器。 连现代浏览器都在用:网页脚本编成中间码,由浏览器的引擎执行。 统一的理解方式:只要在【代码】和【机器】之间垫一层,就能把差异收拢到那一层里。 这一招的名字叫虚拟化——下一节的云,用的也是同一个思路。
五、动手 Demo:在任何地方都能提供相同运行环境的Java虚拟机
Java 不编成本机代码,先编成只有虚拟机能认的中间码。 Python 用的也是同一招:源码 → 字节码 → Python 虚拟机。
import dis
src = "total = 0" + chr(10)
src += "for i in range(1, 6):" + chr(10)
src += " total = total + i" + chr(10)
src += "print(total)" + chr(10)
code = compile(src, "<demo>", "exec")
ops = [i.opname for i in dis.get_instructions(code)]
print("字节码指令", len(ops), "条")
print("指令种类 ", len(set(ops)), "种")
实跑输出:
--------------------------------------------------------------------
在任何地方都能提供相同运行环境的Java虚拟机
--------------------------------------------------------------------
Java 的思路很干脆:**既然各家 CPU、各家系统的代码都不一样,**
**那就别直接编成本机代码,先编成一套【谁都不认、只有虚拟机能认】的中间码。**
角色 作用
--------------------------------------------------------------
Java 源码(.java) 人写的那份
编译器 翻成中间码(.class)
中间码 **不针对任何 CPU,只针对虚拟机**
虚拟机(JVM) 各平台各装一份,负责把中间码执行掉
本机代码 由虚拟机在其中生成,这一层对开发者透明
于是**同一份 .class 文件**,在 Windows、Linux、手机上都能跑——
前提是**那台机器上先装了这个虚拟机。**
有意思的是:**Python 用的也是同一招。**
它把源码编成字节码,再交给 Python 虚拟机执行。
实测:看一段 Python 源码的字节码,**数一数它是哪种【指令集】。**
源码 4 行
字节码指令 24 条
指令种类 13 种
BINARY_OP CALL END_FOR FOR_ITER
GET_ITER JUMP_BACKWARD LOAD_CONST LOAD_NAME
POP_TOP PUSH_NULL RESUME RETURN_CONST
STORE_NAME
**这些指令不是任何 CPU 的指令,而是 Python 虚拟机的指令。**
换到别的 CPU 上,**字节码不用改,改的只是那台机器上的虚拟机。**
一句话总结这套办法的好处:**一次编译,到处运行;代价是多了一层执行开销。**
这 24 条指令不是任何 CPU 的指令,而是 Python 虚拟机的指令。 换到别的 CPU 上,字节码不用改,改的只是那台机器上的虚拟机。
30 秒实验
- 查一查 Java 的 .class 文件是什么,它和 Python 的字节码是一回事吗。
- 想一想:为什么手机上的 App 常要针对机型分别打包。
常见误区
- 以为 Java 是【解释执行所以永远慢】。热点代码会在运行时编成本机代码。
- 以为虚拟机是【模拟一台完整的电脑】。它执行的是中间码,不是模拟硬件。
自测三问(点开对答案)
1. Java 是怎么做到一次编译到处运行的? —— 编成不针对任何 CPU 的中间码,各平台各装一份虚拟机来执行它。 2. 虚拟机方案的代价是什么? —— 多一层导致性能与内存开销,且目标平台必须能装上虚拟机。 3. 为什么说 Python 也用了同一招? —— 它把源码编成字节码,再交给 Python 虚拟机执行,换 CPU 时字节码无需改动。- ☐ 下一节(7.6 云计算平台提供的虚拟运行环境)
相关笔记
- 使用源代码进行安装 — 编译产物决定能不能跨平台
- 云计算平台提供的虚拟运行环境 — 同一种思想的更大尺度
- 用二叉查找树高效地查找数据 — 一层层抽象是全书的老套路
- 🖥️ 服务器工作原理 · 索引 — 服务器怎么攒出来的
- ⚙️ 程序是怎样跑起来的 · 索引