Skip to main content
  1. Posts/

运行时系统初步认识

·481 words·3 mins·
Eric Linus
Author
Eric Linus
北京邮电大学软件工程专业本科生,主要语言C++,对系统编程,数据库和AI系统交叉感兴趣。熟悉C++/Python/C#/Java/Rust。Github:@n00bme0w
Table of Contents
mindmap
  root((Runtime Systems
生态位图谱)) 编译型高性能 JVM Java/Scala/Kotlin 企业级应用 大数据处理 长期运行服务 Go Runtime 云原生基础设施 微服务 高并发网络服务 .NET CLR Windows生态 企业应用 游戏开发 解释型灵活开发 Python CPython AI/ML领域 快速原型 脚本自动化 Ruby MRI Web开发 快速迭代 开发者友好 PHP Zend Web应用 CMS系统 中小型网站 JavaScript生态 V8 Node.js 全栈开发 实时应用 前后端统一 Deno 安全沙箱 现代JS/TS 分布式工具 新兴语言运行时 Rust 无运行时 系统编程 WebAssembly 安全关键系统 Swift Runtime Apple生态 移动应用 服务端 函数式运行时 Erlang BEAM 电信系统 高可用系统 即时通讯 Elixir Web实时应用 并发系统 可扩展服务

基础与定义
#

  • A run-time system is defined as a software component responsible for dynamic management of program execution and of the resources granted by the OS.

  • In other words, an RTS mediates interaction between the application and the OS.

  • Different from run-time environment: A RTS is distinct from and embedded into a more broad runtime environment, which is defined to include the OS that is responsible for overall resource management, and RTS instances managing other running applications unaware of each other’s existence and resource demands

是一个介于操作系统和上层应用之前的软件组件,其核心功能是为程序运行提供必要依赖支持,包含运行库的实例化调用及运行时系统的资源管理,贯穿程序生命周期中的执行阶段。

运行时分类
#

1. 运行时库vs运行时虚拟机
#

运行时库:一组在运行阶段起关键作用的库,负责程序启动、初始化、标准库实现、与 OS 交互、异常处理、清理退出等。如printf,malloc/free以及真正的入口_start就来自运行时库。

2. 托管运行时vs非托管运行时
#

托管运行时: 程序跑在一个“虚拟机 / 托管执行环境”里,运行时在中间“管”着你的代码,提供自动内存管理(GC)、类型安全检查、安全沙箱、异常处理等服务。典型例子:JVM、.NET CLR、JavaScript 的 V8 等。 非托管运行时系统:程序直接跑在操作系统和 CPU 上,运行时只是“帮手”,提供启动代码、标准库函数等,但不帮你“管”内存安全、类型安全这些事。典型例子:C/C++ 的 C 运行时库(CRT)、各种 native runtime。

运行时系统的核心功能
#

软件生命周期中的运行时系统
#

RTS最初是在程序运行时发挥作用,程序运行时包括启动,初始化,执行和终止阶段,包括潜在的动态链接,加载和JIT/AOT编译。

C的最小RTS crts0是被连接器/加载器添加,然后初始化执行环境(栈/堆栈指针/寄存器/信号处理器),通过运行main函数把控制移交给程序,最终将main的返回值传递给运行时环境。

crts0

由于crts0是平台特定的,并由汇编实现的,在不同架构上就需要重新编译,因此作为“抽象机器”的功能没有被提供。

更精密的RTS要么被实现为一个独立的库,被链接进应用的object code来创建一个可执行程序,要么他们自身就是一个接受程序代码作为输入的应用程序,这个输入通常是解耦了符号编程语言和目标架构原生指令集的IR形式,之后会通过解释或JIT编译成原生代码。

结构和功能
#

RTS的主要功能是在基于特定执行模型实现的抽象机器上管理程序的执行,它要包括和OS交互,内存管理,线程和任务管理,通信和监视等

执行模型:
#

最常用的模型是stack machine,一个栈用来存放函数参数以及函数调用的中间结果。这个顺序模型和register machine(栈被一系列映射到机器寄存器的虚拟寄存器替代)通常被命令式和面向对象语言采用。

还有一种有名的模型是graph reduction,通常被基于lambda演算的函数式语言使用,该模型适合并发,因为图的部分可以被独立规约。还有term-rewriting模型…

OS交互
#

RTS依赖OS来进行资源分配,IO和网络以及提供一个掩盖操作系统细节同时提供架构独立的API给程序员。RTS通常直接实现高级语言的特性和难以在语言自身中直接实现的低级语言原语。

错误和异常处理
#

通过全局errno变量来进行错误处理是很简陋的。更现代的语言通常提供自定义的信号处理和基于try/catch机制的异常处理,这通常导致复杂的控制流,精密的RTS可以在运行时透明的处理错误。

内存管理
#

大多数高级语言需要RTS提供GC能力来自动处理堆对象内存的分配和回收。常见的GC策略有引用计数式和追踪式两大类。GC还会通过compacting周期性将已使用的对象聚集在一起来避免内存碎片化,通过generational基于“存活越久的对象越有可能继续存活,而大多数对象会在短时间内消亡”将长期存活的对象提升到回收频率低的堆区域。在多核架构上一个潜在的问题是Stop the world停顿影响性能。

reference-counting:引用计数GC,通过对持有对象的引用进行简单计数,计数为0时释放,缺点,单独使用时会发生循环引用内存泄露。

引用计数式:

python = reference-counting + generationalGC
#
  • 在引用技术基础上通过分代GC去除循环引用:将对象分为多代,一- 开始新创建的对象都是第0代,当 $$分配数-释放数>阈值$$对所有第0代对象执行一次循环GC,将经历过这次GC的对象提升到下一代,同时对部分老年代做GC。

GC算法:选出一批要要扫描的容器对象,给每一个对象一个临时计数,初始化为其引用计数数目,遍历所有容器,对每个容器遍历它引用的所有对象,将这些对象的临时计数-1,此时若一个容器对象计数>0,说明它被外部引用,它是可达的。若计数==0,说明只有集合内部的对象引用它,但是这个集合内部对象可达不可达未知,所以再遍历一次,从所有计数>0的对象出发,把所有到达的对象标记为计数设为1,此时计数仍未0的对象就是不可达循环引用环的成分。

追踪式:

GO
#

早期Mark And Sweep:

Mark阶段从Roots往下逐渐标记,没有被标记到的就是无法被访问到的垃圾内存

Sweep阶段统一清除垃圾内存

后期三色标记法:

白色:尚未被访问

灰色:被访问到,但是它引用的对象还没有被完全扫描

黑色:被访问到,且它引用的对象被扫描完

Mark阶段:

将根标记为灰色,循环取出灰色的节点A直至没有灰色节点,将A引用的节点标记为灰色,当标记完A所有节点时A变为黑色。

Sweep阶段:

清除所有GC后依然为白色的节点。

为了防止全程STW,引入写屏障来支持并发GC

危险情况:

  1. 黑色对象引用了白色对象。
  2. 灰色对象对白色对象的引用被删除。

并发时如果GC进行了一部分,同时有1和2发生,就会导致有正在使用的对象被当成垃圾内存回收掉。

例如:

a, c are the roots.

a -> b -> s1, s2, s3;

c -> d;

如果b刚变为黑色,c把d引用传给了b,即b->s1,s2,s3,d 此刻,b是黑色,d是白色,还没有GC到,如果没有其他事情发生,d会被通过c扫描到,然后变为灰色和黑色。 但是恰好此刻c断开对d的引用。那么d将无法被GC扫描到,GC结束时d是白色,会被错误清除。

必须维护三色不变式:

强三色不变式:
    不允许任何黑色对象引用白色对象。
    一旦满足,就绝不可能漏标白色对象。
弱三色不变式:
    黑色对象可以引用白色对象,但这些白色对象必须被其它灰色对象引用(或存在一条从灰色对象到它的路径)。
    只要“灰色 → 白色”的路径在,白色对象最终就会被扫描到。

两个不变式中任何一个成立就可以防止GC错误删除有用的对象。

GO引入写屏障保障并发安全性

插入写屏障:
    若正在GC,当对象 A 增加一个指向 B 的引用时,把 B 标记为灰色。
删除写屏障:
    若正在GC,当对象 A 断开对 B 的引用时,如果 B 是灰色或白色,就把 B 标记为灰色。
ART
#

ART将内存划分为多个空间

混用多种GC算法:包括Mark Sweep,Semi Space,Mark Compact和分代法。

Semi Space: 堆分为等大的From和To空间,GC时将From空间的存活对象复制到To空间。 To空间变为新From空间,原From空间清空

Mark Compact: 先做标记,再将存活对象向堆的一端移动,消除碎片,使空闲空间连续,然后遍历修正所有对象引用地址。

线程和任务管理
#

操作系统级别的threads和RTS级别的tasks(lightweight threads),是支持并发和并行的核心。RTS有时管理一个线程池和一个任务池,将一组任务多路复用到一组操作系统线程上来实现拓展性。许多语言提供显式线程编程模型,然而由于存在可观察的非确定性以及静态条件和死锁问题,这种模型被认为是一个糟糕的选择,因此诸如事务内存以及半显式或基于骨架的模型等更高级别的编程模型获得了越来越多的关注。

显式的同步原语还引起了粒度的权衡,这对可移植性能有害:全局锁安全,但牺牲并行性;细粒度锁会导致难以承受的开销。执行协调,包括跨异构架构的调度和工作分配,显著增加了RTS决策复杂度。这就是可选任务/线程优于强制任务/线程的原因:RTS仅在判定值得时才会并发执行任务。

两个主要的工作分发机制是work pushing,急切地卸载工作和work stealing,工作分配由需求驱动。

通信
#

大规模的软件需要的可拓展性超出了单节点的范畴,这要求必须将工作分发到集群/网格/云端。两种常见的方案包括shared memory(可以是分布式,需要额外的抽象层)和message passing。如果通信跨越网络进行,则需要对计算和数据进行序列化。对于共享上下文信息的分布式协作RTS来说,发布/订阅等更复杂的协议可能更好。

监控
#

性能分析,调试和执行重放依赖于追踪(即对相关事件的记录),而动态优化则依赖运行时通过监控获取的系统信息。常见的性能分析策略按开销递增依此为:累计汇总统计,普查式统计,细粒度基于时间统计。

Runtime 生态概览
#

一、JVM:企业级与大数据的主流选择
#

核心场景:

  • 传统企业后端(银行、电商)、微服务架构、中间件。
  • 大数据平台(Hadoop/Spark)及数据工程。 生态现状:
  • 成熟稳定: 发展二十余年,工具链完善,配合 Spring 框架是企业级后端的事实标准。
  • 云原生挑战与优化: 针对传统 JVM 启动慢、内存占用高的问题,目前通过 GraalVM Native Image 实现原生编译,以及厂商优化 JDK(如 Azul)来适应 Serverless 和容器场景。 定位: 坐拥“企业后端 + 大数据”基本盘,正向云原生场景延伸,但在轻量级高并发领域面临 Go/Rust 的竞争。

二、V8 / JavaScript 运行时:覆盖面最广的跨端生态
#

核心构成:

  • 浏览器引擎: V8(Chrome/Edge)、JavaScriptCore(Safari)等,垄断 Web 前端。
  • 服务端与边缘: Node.js 存量巨大;Deno、Bun 等新一代运行时追求更快的启动速度与原生 TypeScript 支持;边缘运行时主要用于低延迟短任务。 生态现状:
  • 前端地位不可撼动。
  • 后端在 BFF、API 服务及工具链领域普及度高。
  • 边缘计算与无服务器场景增长迅速。 定位: “前端统治 + 后端稳固 + 边缘扩展”,是目前覆盖场景最广泛的运行时生态。

三、CLR / .NET Runtime:微软系的跨平台企业运行时
#

核心场景:

  • Windows 生态企业系统(ERP/CRM)、ASP.NET Core Web API。
  • 桌面应用、游戏开发及云原生应用。 生态现状:
  • 深度整合: 与 Visual Studio、Azure 云服务紧密绑定,开发体验好。
  • 现代化演进: .NET 5+ 统一并实现了真正的跨平台。.NET 8 在性能、GC 和 AOT 编译上优化显著,已转型为现代云原生应用基础平台。 定位: 类似 JVM 的企业级运行时,深耕微软技术栈,正向跨平台与高性能云原生方向演进。

四、ART(Android Runtime):移动端基础设施
#

核心场景: Android 应用执行(Java/Kotlin 编译为 dex/oat)。 生态现状:

  • Android 系统的唯一官方运行时,深度绑定系统底层,管理应用沙箱、权限与内存。
  • 属于操作系统级抽象,不具备通用性,但在 Android 生态内地位不可替代。 定位: 移动端应用执行的底层基石。

五、动态语言运行时:以开发效率与生态取胜
#

1. Python (CPython)
#

  • 场景: AI/数据科学、自动化脚本、Web 后端。
  • 特点: 解释执行,性能虽不及 JVM/V8,但胜在科学计算库生态极其丰富。
  • 优化: 通过 PyPy JIT 或 C/Rust 扩展弥补性能短板。

2. PHP (Zend Engine)
#

  • 场景: 传统 Web 后端、CMS(如 WordPress)。
  • 特点: 部署简单,在内容类站点中存量巨大。 定位: 不追求极致性能,核心优势在于庞大的库生态与高效的开发效率,分别在 AI 领域和 Web 内容领域占据强势地位。

六、WebAssembly 运行时:新兴的跨语言基座
#

核心特性: 二进制指令格式,具备可移植、沙箱化、接近原生性能的特点。通过 WASI 扩展至非浏览器环境。 核心场景:

  • 浏览器侧: 复杂图形、音视频处理、AI 推理等高性能模块。
  • 服务端/边缘: 作为安全沙箱,支持多语言模块混合运行,适用于 Serverless 与边缘计算。 定位: 快速成长的新兴生态。并非直接替代传统运行时,而是作为浏览器的高性能补充和服务端的多语言安全运行基座。