尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

代码是如何运行起来的:编译器、解释器与虚拟机

代码是如何运行起来的:编译器、解释器与虚拟机 程序员每天都在点击“运行”但源代码从屏幕上的文字变成真正执行的程序中间发生了什么往往没有这个按钮看起来那么简单。处理器并不认识 Python 的缩进、Java 的类也不理解 JavaScript 中的async。它能执行的是由指令集规定的一系列机器指令例如读取数据、完成加法、比较两个数或者跳转到另一个位置。高级编程语言与处理器之间存在巨大的表达差异而编译器、解释器和虚拟机就是连接两者的桥梁。人们常把语言分成“编译型”和“解释型”然后得出一些简洁的结论编译型语言运行快解释型语言开发方便虚拟机则处于两者之间。这种分类适合入门却不足以描述现代程序的真实执行过程。今天的语言实现经常同时使用编译、解释、缓存和即时优化一段代码甚至会在运行过程中经历多种形态。理解这些过程不只是为了学习编译原理。它能帮助我们解释程序为什么首次启动较慢、为什么修改代码后必须重新构建、为什么同一份 Java 程序可以运行在不同系统上以及为什么某些错误在运行之前就会被发现。源代码只是给人看的表达假设我们写下一个简单函数deftotal(price,count):returnprice*count人类可以直接看出它在计算总价但计算机首先看到的只是一串字符。要理解这段代码语言工具需要识别def是关键字total是函数名括号内是参数缩进后的语句属于函数体。然后它还要判断这些元素之间是否符合语言规则。这个过程通常从词法分析开始。词法分析器把连续字符划分成一个个有意义的记号例如关键字、标识符、数字和运算符。接下来的语法分析会按照语言的语法规则组织这些记号形成抽象语法树。它不是把代码原样换成树形结构而是保留程序真正关心的关系这是一个函数定义函数体包含返回语句返回值来自一次乘法运算。抽象语法树让后续处理不必继续面对零散字符。编译器可以在树上检查变量是否存在、函数调用是否合理也可以将不同写法转换为统一的内部形式。开发工具同样会利用类似结构完成代码格式化、跳转、重构和静态检查。这里可以看出一个容易忽略的事实源代码的主要读者其实是人。变量名、换行、注释和模块结构让程序便于理解和维护而机器最终需要的是更加严格、更加接近执行模型的表示。语言工具的任务就是在保留程序含义的前提下完成这种转换。编译器不只是“翻译器”以 C、C 或 Rust 为代表的常见流程会在程序运行前生成目标平台能够执行的机器码这通常被称为提前编译。它并不是把一行源代码机械地翻译成一条机器指令而是包含多个阶段。编译器先完成语法和语义检查再把程序转换成适合分析的中间表示。中间表示比源代码简单、统一又不像某种处理器的机器指令那样固定因此方便进行优化。同一套编译器前端可以理解不同语言后端则可以针对 x86、ARM 等不同架构生成代码。优化阶段会尝试在不改变程序结果的前提下减少不必要的工作。例如intresult6*7;如果两个操作数在编译时已经确定编译器可以直接把结果视为42不必让程序在每次运行时重新计算。对于没有实际作用的代码编译器可能将其删除对于频繁调用的小函数也可能把函数内容直接放到调用位置以减少调用成本。优化并不是简单地“把所有代码变快”。一种改写可能降低运行时间却增加生成文件的体积针对某种处理器生成的指令也可能不适合另一种处理器。编译器需要在速度、体积、构建时间和调试体验之间权衡这也是构建工具通常提供不同优化级别的原因。生成目标代码后程序往往还不能直接运行。大型项目由多个源文件和第三方库组成每个文件可以先被编译成目标文件再由链接器将它们组合起来。链接器需要找到函数和全局数据的最终位置解决“这里调用的函数究竟位于哪里”之类的问题。静态链接可以把依赖代码放进最终程序动态链接则允许程序在运行时使用系统中的共享库。因此日常所说的“编译”经常是整套构建流程的简称其中可能包含预处理、编译、汇编和链接。某个项目出现构建错误时区分问题发生在哪个阶段通常比反复修改代码更有效。提前编译的优势是许多工作已经在运行前完成程序启动后可以直接执行机器码也更容易针对硬件进行深度优化。代价则是生成结果通常依赖操作系统和处理器架构。为电脑生成的可执行文件不能直接放到手机上运行为 x86 架构构建的程序也不能自然变成 ARM 程序。所谓“跨平台”很多时候意味着同一份源代码能够在不同平台重新编译而不是同一个二进制文件可以到处执行。解释器并非简单地逐行翻译解释执行常被描述为“读一行运行一行”。这个说法能够帮助初学者建立直觉却很容易造成误解。现代解释器通常会先解析完整代码生成抽象语法树或更紧凑的字节码然后再执行。否则循环每运行一次都要重新理解同一段文本效率会非常低。以常见的 Python 实现 CPython 为例Python 源代码会先被编译为字节码。字节码不是特定处理器的机器指令而是一组供 Python 虚拟机理解的操作。虚拟机读取这些操作维护函数调用、局部变量和异常状态再完成对应行为。这解释了为什么 Python 同样存在“编译”步骤。它只是通常不直接生成 CPU 可以执行的本地机器码而是生成更适合自身运行时执行的中间形式。缓存字节码还可以避免程序下次启动时重复完成部分工作但它不会把 Python 程序变成脱离运行环境的本地应用。解释器的价值在于灵活。它可以在运行时创建新对象、加载模块甚至执行动态生成的代码。交互式命令行也可以立即接收表达式并返回结果这对数据探索、自动化脚本和快速实验十分方便。相应地执行每个字节码操作时虚拟机通常需要完成指令分派和动态类型检查成本会高于直接运行经过充分优化的机器指令。不过不能据此断言“使用解释器的语言一定很慢”。程序的实际性能还取决于算法、运行时实现和库。Python 的科学计算代码经常把大量工作交给由 C、C 或其他底层语言实现的库Python 本身主要负责组织流程。表面上是一段 Python 程序真正消耗时间的计算可能早已进入经过优化的本地代码。虚拟机建立了一台软件计算机字节码需要一个执行环境这就是虚拟机。这里的虚拟机并不一定是模拟完整电脑的系统虚拟机它更像一台为某种语言或运行平台设计的软件计算机。它定义自己的指令、栈、内存模型和异常机制再由不同操作系统上的实现负责把这些操作落实到真实硬件。Java 是这种路线最典型的代表。Java 源代码通常先由编译器转换为 JVM 字节码字节码再交给 Java 虚拟机执行。Windows、Linux 和 macOS 使用各自平台上的 JVM因此同一份字节码可以在多个系统上运行。跨平台能力不是因为硬件突然理解了 Java而是因为每个平台都安装了能够理解同一套中间指令的翻译层。虚拟机还能提供许多语言之外的工程能力。它可以统一管理内存、回收不再使用的对象验证即将加载的字节码记录线程状态并为监控和调试提供接口。程序获得了相对一致的运行环境开发者也不必为每个平台重复实现这些基础设施。当然这层抽象并非没有成本。程序启动时需要加载运行时和类字节码执行也需要额外处理。虚拟机因此常与即时编译器配合先快速启动和执行程序再根据运行情况把重要部分编译成本地机器码。即时编译根据真实运行情况优化提前编译器能够仔细分析代码却无法总是预知程序运行时会收到什么数据、哪条路径最常使用。即时编译也就是 JIT利用程序已经开始运行这一条件收集信息。虚拟机会记录哪些函数被频繁调用、某个分支通常走向哪边以及某个位置经常出现什么类型的数据。当一段代码成为“热点”后JIT 可以把它编译成针对当前机器和真实使用情况优化的本地代码。之后再次执行这段逻辑就不必继续逐条解释字节码。这种策略经常采用推测优化。假设某个加法操作在过去总是接收整数JIT 可以生成专门处理整数的快速代码而不必每次检查所有可能类型。如果后来出现了不同类型运行时会放弃这份过于乐观的机器码退回更通用的执行方式这个过程称为反优化。即时编译在 Java 虚拟机和现代 JavaScript 引擎中十分常见。浏览器加载 JavaScript 后需要尽快让页面可用因此引擎可能先用启动成本较低的方式执行代码再逐步优化热点。这样既避免等待整个程序完成深度编译又能提高长期运行代码的性能。它的代价也很明确。性能分析、机器码生成和反优化都发生在程序运行期间会占用处理器和内存。同一段代码在刚启动时和运行一段时间后性能可能并不相同。服务器应用可以通过长期运行摊薄预热成本命令行小工具却可能在优化收益出现前就已经结束。因此执行方案必须结合程序的生命周期判断。现代语言正在模糊传统边界现实中的语言很少只走一条路线。Java 先编译为字节码再由虚拟机解释和即时编译JavaScript 引擎会解析源码、生成内部表示并对热点代码进行优化Python 实现既可以执行字节码也可以通过其他实现或扩展模块使用即时编译和本地代码。甚至提前编译的程序在运行时也仍然需要动态链接器、系统库和语言运行时的支持。所以“这门语言是编译型还是解释型”并不是一个总能得到唯一答案的问题。语言是一套语法和语义规则编译或解释则是实现这些规则的方法。同一门语言可以拥有多个实现不同实现也可以选择完全不同的执行策略。更有价值的问题是工作在什么时候完成如果大量分析发生在构建阶段运行时通常更直接但构建时间会增加结果也可能依赖具体平台如果工作推迟到运行阶段系统能够利用真实数据并保持动态能力却需要承担启动、内存和运行时管理成本如果使用统一字节码和虚拟机程序更容易跨平台同时依赖目标环境提供相应运行时。这些取舍最终会影响开发体验。提前编译能够在部署前发现更多问题却可能让修改后的反馈变慢解释环境适合快速试验却把一部分错误留到运行阶段即时编译能够获得更贴近真实场景的优化却使性能变化更加复杂。没有哪一种方案在所有维度上都占优势。从“运行”按钮重新理解程序当我们再次点击“运行”时背后可能发生的是一条完整链路源代码被分解为记号语法结构被组织成树程序转换为中间表示或字节码优化器尝试删除多余工作链接器组合不同模块虚拟机建立运行环境即时编译器再根据热点生成机器码。最终处理器执行的仍然只是它能够理解的指令。编译器、解释器和虚拟机并不是互相排斥的三种产品而是不同阶段可以组合使用的技术。它们共同解决一个核心问题怎样让适合人类表达和维护的代码可靠而高效地运行在真实机器上。理解这段旅程之后许多看似独立的现象会连在一起。构建速度、启动时间、峰值性能、跨平台能力和动态特性都与代码在哪个阶段被理解和优化有关。程序语言之所以拥有不同的执行方式不是因为某种方案尚未战胜其他方案而是因为软件依然需要在开发效率、运行性能、可移植性和复杂度之间不断选择。
返回列表