字节码是【一次编译、到处运行】的中间格式;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,即时编译)的思路是:盯梢,抓典型,重点翻译。
- 先解释:程序启动,逐条解释字节码(启动快,不用等编译);
- 数数:运行时统计每段代码被执行的次数;
- 抓热点:某段代码被反复执行(HotSpot 默认阈值约一万次调用),触发即时编译——现场把它翻译成本机器的机器码;
- 换挡:之后再执行这段代码,直接跑机器码,跳过解释。
名字的来历:这套策略就叫 HotSpot(热点)——Java 的虚拟机干脆以此命名。V8 引擎(Chrome/Node.js)更夸张,分四层梯队:Ignition(解释器)→ Sparkplug(基线 JIT)→ Maglev(中层优化)→ TurboFan(全力优化),代码越热爬得越高。
三、代价与陷阱:天下没有免费的翻译
JIT 的账单有两笔:
- 预热(warmup):启动后的头几秒,代码还在被解释+被编译,性能低于稳态。所以【Java 程序跑久了越来越快】是真事——那是 JIT 换挡完毕。短命程序(跑一次就退的命令行工具)可能永远享受不到稳态速度;
- 去优化(deoptimization):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 的梗)。你看到的不是乱码,是【面向虚拟机的汇编】的文件头。
六、常见误区
- 【字节码是半机器码,比源代码快很多】——错。字节码本身不被 CPU 执行,快慢取决于 VM 怎么处理它:纯解释(CPython)提升有限,JIT(HotSpot/V8)才有数量级提升。
- 【JIT 就是提前编译(AOT)】——反了。AOT(提前编译,如 C/Rust/Go)在运行前把全部代码翻译完;JIT 在运行中边跑边翻、只翻热点。两者正相反(GraalVM Native Image 那类【AOT 化的 Java】是混合特例)。
- 【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——格式相同,实现策略不同。- ☐ 下一节(2-10)
相关笔记
- Java 1.4 · 运行机制 —— .class 与 JVM 的正面讲解
- · 编译器与解释器 —— 光谱的两端,本节合龙
- · 比较一 —— 四条编译目标的完整表格
- ⌨️ 编程原理 · 索引 —— 全课路线图