⚙️ 第七章 程序在怎样的环境下运行
📄 一句话概括

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 秒实验

  1. 查一查 Java 的 .class 文件是什么,它和 Python 的字节码是一回事吗。
  2. 想一想:为什么手机上的 App 常要针对机型分别打包。

常见误区

  1. 以为 Java 是【解释执行所以永远慢】。热点代码会在运行时编成本机代码。
  2. 以为虚拟机是【模拟一台完整的电脑】。它执行的是中间码,不是模拟硬件。
自测三问(点开对答案) 1. Java 是怎么做到一次编译到处运行的? —— 编成不针对任何 CPU 的中间码,各平台各装一份虚拟机来执行它。 2. 虚拟机方案的代价是什么? —— 多一层导致性能与内存开销,且目标平台必须能装上虚拟机。 3. 为什么说 Python 也用了同一招? —— 它把源码编成字节码,再交给 Python 虚拟机执行,换 CPU 时字节码无需改动。

相关笔记