⌨️ 第二章 编程语言都有什么不同
📄 一句话概括

字节码是【一次编译、到处运行】的中间格式;JIT(即时编译)在程序运行时盯着哪些代码跑得最勤,把它们现场翻译成机器码——解释型的启动灵活与编译型的稳态速度,就这样被同时拿到手。

📝 本节和前面几节的关系

这是第二章的算术高峰(难度 ⭐⭐⭐):2-1 的编译器/解释器光谱在此合龙,2-6 埋的【Java 字节码】、2-1 埋的【__pycache__】在此拼完最后一块。Java 课 1.4 讲过【编译→class→JVM】,本节把【JVM 拿到字节码之后干了什么】讲透。

一、字节码:面向虚拟机的【汇编】

字节码(Bytecode)是一种中间格式:不是给 CPU 看的(机器码),也不是给人看的(源代码),是给虚拟机看的。

流程(以 Java 为例,回收 Java 课 1.4):

Hello.java  --(javac 编译)-->  Hello.class(字节码)
                                    |
        Windows 的 JVM ------------ 双手奉上 ------------ Linux 的 JVM
        (翻译成 x86 机器码)                        (翻译成 ARM 机器码)

源代码只编译一次;各平台的 JVM 负责最后一程——这就是【一次编写,到处运行】的全部秘密(回收 2-2 二进制跨平台)。同一个思路的家族成员:

平台 字节码 虚拟机
Java .class(打进 .jar) JVM
C# CIL(IL 中间语言) CLR
Python .pyc(__pycache__ 里,回收 2-1 的 dis 实验) CPython 虚拟机
JS(V8) Ignition 字节码 V8

二、JIT:运行时的【热点编译器】

拿到了字节码,虚拟机怎么执行?最笨的办法是逐条解释(慢)。JIT(Just-In-Time,即时编译)的思路是:盯梢,抓典型,重点翻译。

  1. 先解释:程序启动,逐条解释字节码(启动快,不用等编译);
  2. 数数:运行时统计每段代码被执行的次数;
  3. 抓热点:某段代码被反复执行(HotSpot 默认阈值约一万次调用),触发即时编译——现场把它翻译成本机器的机器码;
  4. 换挡:之后再执行这段代码,直接跑机器码,跳过解释。

名字的来历:这套策略就叫 HotSpot(热点)——Java 的虚拟机干脆以此命名。V8 引擎(Chrome/Node.js)更夸张,分四层梯队:Ignition(解释器)→ Sparkplug(基线 JIT)→ Maglev(中层优化)→ TurboFan(全力优化),代码越热爬得越高。

三、代价与陷阱:天下没有免费的翻译

JIT 的账单有两笔:

四、动手 Demo:热循环与字节码的现场

第一步:看一段【热点代码】长什么样(重复一万次的小循环,正是 JIT 的标准猎物):

def hot_loop(n):
    total = 0
    for i in range(n):
        total += i
    return total

print(hot_loop(10_000))     # 热点:调一次就循环一万次

第二步:亲手造一个字节码文件(回收 2-1 的 __pycache__,这次不靠 import 触发,直接下令):

import py_compile, tempfile, os, glob

src = os.path.join(tempfile.gettempdir(), "demo_bytecode.py")
with open(src, "w", encoding="utf-8") as f:
    f.write("print('我来自字节码')")

py_compile.compile(src)     # 显式编译成字节码
cache = glob.glob(os.path.join(tempfile.gettempdir(),
                               "__pycache__", "demo_bytecode*.pyc"))
print("生成的字节码文件:", cache)

看到什么算成功(版本号随你的 Python 而变):

我来自字节码
生成的字节码文件: ['C:\\Users\\你\\AppData\\Local\\Temp\\__pycache__\\demo_bytecode.cpython-313.pyc']

读结果:.pyc 文件被亲手造了出来——它就是 Python 世界的【字节码产物】。注意:CPython 对 .pyc 只解释、不 JIT(PyPy 才是带 JIT 的 Python 实现,长循环能快一个数量级);Java/C#/JS 的虚拟机才标配 JIT。同样的字节码,VM 可以选择解释、也可以选择 JIT——这是实现策略,不是格式规定。

五、30 秒实验

打开你 Java 课的工作目录,找一个 target 或 out 目录里的 .class 文件,用记事本强行打开盯 30 秒——开头四个字节是 CA FE BA BE(咖啡宝贝,Java 字节码的魔数,咖啡杯 logo 的梗)。你看到的不是乱码,是【面向虚拟机的汇编】的文件头。

六、常见误区

  1. 【字节码是半机器码,比源代码快很多】——错。字节码本身不被 CPU 执行,快慢取决于 VM 怎么处理它:纯解释(CPython)提升有限,JIT(HotSpot/V8)才有数量级提升。
  2. 【JIT 就是提前编译(AOT)】——反了。AOT(提前编译,如 C/Rust/Go)在运行前把全部代码翻译完;JIT 在运行中边跑边翻、只翻热点。两者正相反(GraalVM Native Image 那类【AOT 化的 Java】是混合特例)。
  3. 【Python 有 __pycache__ 所以 Python 也有 JIT】——错。__pycache__ 缓存的只是字节码,CPython 至今默认纯解释(3.13 起有实验性 JIT 但默认关闭);Java/JS/C# 的运行时才是 JIT 标配。
自测三问(点开对答案) 1. **字节码的定位是什么?它和机器码、源代码的区别?** 答:面向虚拟机的中间格式;机器码给 CPU、源代码给人、字节码给 VM——一次编译,各平台 VM 各自翻译执行。 2. **JIT 的四步工作法与它的两笔代价?** 答:先解释→统计调用次数→热点(约一万次)现场编译成机器码→后续直接跑;代价是启动预热与假设被打破时的去优化。 3. **同样有字节码,为什么 Java 稳态快而 CPython 慢?** 答:HotSpot 对字节码做分层 JIT(热点翻译成机器码),CPython 默认只解释字节码不 JIT——格式相同,实现策略不同。

相关笔记