本节摘要:即时编译把编译搬进运行时:解释器先跑起来,热点函数被探测后编译成机器码替换执行,优化假设被打破时去优化退回安全版本。本节走完 JIT 的完整闭环,看运行时信息(类型分布、分支频率、调用目标)如何换来静态编译器拿不到的优化,最后盘点分层编译与 AOT 的合流趋势。
阅读完本节,你应当能够:
第 1.2 节埋的伏笔在此展开。JIT 的运行图景是一个四拍循环:
第一拍 解释预热: 程序启动,解释器逐条执行字节码,同时为每个函数/循环背计数器 第二拍 热点探测: 计数器超阈值(如函数调用上千次、循环回边上万次) → 判定为热点,提交后台编译 第三拍 编译替换: 后台线程做优化加代码生成(第 5、6 章的全套,外加运行时信息) 编译完成后修改调用入口,下次进入直接执行机器码 第四拍 去优化兜底: 若优化时的假设被打破(类型变了、新类加载了) → 丢弃优化版本,回退解释器或保守版本,状态还原后继续 程序语义永远以解释器的语义为准,机器码只是"被证明安全的快车道"
第四拍是 JIT 与静态编译器的本质分野。静态编译器面对未知输入只能保守;JIT 大胆假设("这个加法的两个操作数一直是整数"),因为假设被打破时有退路——去优化保证大胆的代价有上界。
用伪代码把闭环钉死,四个转换点一目了然:
函数入口: 计数器 加一 若 计数器 小于 一万 且未编译: 走解释器路径 若 计数器 达到 一万 且未提交: 提交后台编译,继续解释执行 若 机器码已就绪: 跳转机器码入口 机器码内部: 每个优化假设前有守卫检查 守卫:两操作数均为整数标签? 是 → 整数加法指令,直通快车道 否 → 触发去优化:现场还原到解释器状态,回退继续 后台线程: 取字节码 加 运行时类型档案 → 类型特化 加 分支裁剪 加 第5第6章全套 → 生成机器码 加 守卫,原子替换入口指针
类型特化。动态语言的加法是出了名的慢:解释器要先查两个操作数是整数、浮点还是字符串再分派。JIT 观察一万次加法全是整数,直接生成整数加指令并附上类型守卫(检查不匹配即去优化)——动态语言的算术接近静态语言的速度,全靠这一招。主线语句在动态语言里的旅程再次上演:第一次执行走通用加法路径,热点后编译成的机器码与本教程第 6 章的汇编几乎一样。
分支频率裁剪。静态编译器对 if 两臂一视同仁;JIT 观察到某分支十万次只走过一次,可以把冷分支挪远(甚至不编译,留着去优化的入口),热路径指令排得紧凑、缓存友好。profile 引导优化(PGO)把这个思想搬回静态世界:先插桩跑一遍收集频率,再带着频率数据重编——本质是"借一次运行时的信息,付两次编译的代价"。

JIT 的编译也要花钱——热点函数停下来等编译(或后台编完前继续解释),峰值性能要预热时间才到。分层编译是标准解:第一层快速生成质量平平的机器码(编译快,立刻回本),观察更久仍热的再升到第二层做激进优化。启动延迟、峰值性能、内存占用(每层都要存代码与元数据)三者构成 JIT 的调参三角,各家运行时的默认分层都 是这三者的折中。
JIT 的新课题也在积累。安全上,运行时生码意味着攻击面从"静态二进制"扩展到"动态生成器",JIT 喷雾攻击促使运行时引入 W xor X(写与执行互斥)等防护。生态上,JVM 的即时编译器社区已高度活跃(Graal 用 Java 写 Java 的 JIT),而 .NET 与 Android 的 ART 则走"安装时 AOT 加运行时 JIT"的混血路线——启动用预编译版本,热了再换优化版。
⚠️ 常见坑:拿"马上跑一下"的微基准测 JIT 语言性能。程序还没热身,测的全是解释器速度,结论完全失真;基准测试必须先预热到稳定态再计时。反过来,微基准里被内联优化掉的"被测代码"(死代码消除把整段计算删了)是另一大坑——基准要消费计算结果,防止优化器把考题删了。
💡 关键直觉:JIT 是"带着运行时信息的第 5、6 章"。流水线还是那条流水线——优化、指令选择、寄存器分配,一样不少——只是输入多了探测器送来的情报,输出多了去优化的退路。理解了前面七章,JIT 没有任何新魔法,只有新信息。
问:即时编译的程序能耗与内存代价? 都不小。后台编译线程与主线程抢核,移动设备上表现为发热与掉电;每一层机器码加元数据都要常驻内存,一个大型应用的即时编译缓存可达数百兆。这是即时编译版运行时普遍比预编译版重的原因,也是服务器less场景偏爱预编译(AOT)运行时的直接动机——冷启动时间与内存占用都按毫秒与兆字节计费。
问:能不能把即时编译的成果存下来下次用? 思路叫缓存编译或持久化代码缓存,浏览器与虚拟机都有实现,难点在失效判定:代码版本、运行环境、甚至机器码加载地址变了,缓存就要作废。更彻底的方案是运行时档案反哺(把热点信息存下来供下次预编译参考),它把运行时的情报搬回编译期,是两条时间轴合流的又一个注脚。
补一问:去优化的瞬间开销有多大? 不小。要把优化代码里的执行状态完整还原回解释器的虚拟机状态(哪些变量在哪、执行到哪条字节码),毫秒级停顿都算轻的。所以去优化要少发生:编译时的假设要选稳定属性(类层次封闭、类型不变),并且同一段代码反复去优化会被标记不再优化——宁可慢,不可抖。
(终章)主线语句的旅程与编译技术的地图都到站了。从字符到值,从函数到全程序,从编译期到运行时——带着这条完整的线索,源码与汇编之间不再是黑盒。