本文目录 21 个章节
底层深入-什么是 运行时
# 底层深入-什么是 运行时
什么叫“运行时”?
语言规范负责说“语义”——
for如何展开、异常如何传播等。编译器/字节码生成器负责把源代码翻译成中间形式(JVM 字节码、CIL、Python bytecode、JS bytecode…)。
运行时 = 进程内那段常驻代码 + 数据区,在程序 执行期 解析/执行这些中间形式,并向宿主操作系统或宿主环境(Host)做系统调用。
关键职责:
- 装载(Class Loader / Metadata Loader /
PyImport/ Script Loader) - 执行引擎(解释器循环或 JIT/AOT 生成机器码)
- 内存管理(堆、栈、垃圾回收或引用计数)
- 运行时库(字符串、集合、反射、I/O)
- 原生互操作(JNI、P/Invoke、C-API、Web IDL…)
- 装载(Class Loader / Metadata Loader /
几种运行时
Java 运行时:JVM
核心抽象
- ClassLoader 子系统:按委托模型链式查找
.class→ 填充方法区元数据。(GeeksforGeeks) - Runtime Data Area:线程私有 栈、程序计数器;共享 堆、方法区、运行时常量池。(FreeCodeCamp)
- Execution Engine:包含解释器与 C2/C1 JIT;遇到热点方法即从字节码生成机器码。
- GC:G1/ZGC/Serial GC 等通过写屏障维护分代堆。
伪代码:类加载 + 解释循环
byte[] bytes = readClassFile(name);
Class c = defineClass(bytes); // → 方法区注册
while (pc < bytecode.length) { // 解释器
switch(bytecode[pc++]) {
case IADD: stack.push(stack.pop() + stack.pop()); break;
case INVOKEVIRTUAL:
Method m = cp.resolveMethod(index);
frameStack.push(new Frame(m));
break;
...
}
}
Mono 运行时(.NET CLR 的开源实现)
- Metadata Loader 读取 ECMA-335 元数据表;同 JVM 的常量池。
- JIT/Interpreter/AOT 三种模式可切换(iOS 可静态 AOT)。(DeepWiki, Mono Project)
- GC:SGen(分代、并行压缩)或
libgc;托管对象由 write barrier 追踪引用。 - P/Invoke:自动生成“托管→本地”桩代码让 C# 调 C 库。
伪代码(极简 JIT)
IR *ir = mono_method_to_ir(method);
Code *code = mono_codegen(ir); // x64/arm64 等后端
void* entry = jit_alloc(code->size);
memcpy(entry, code->bytes, code->size);
method->entry = entry;
Python 运行时(CPython)
- PyInterpreterState / PyThreadState 结构体保存全局与线程局部变量。(DeepWiki)
- 字节码虚拟机:名为 ceval loop,对每条
op做栈式运算。 - 内存模型:PyObject 头部引用计数字段,循环引用靠分代 GC 回收。
伪代码:核心执行循环
for (;;) {
int opcode = NEXTOP();
switch(opcode) {
case LOAD_FAST: PUSH(fastlocals[i]); break;
case CALL_FUNCTION:
PyObject *func = POP();
PyObject *args = POP();
PUSH(PyObject_Call(func, args, NULL)); break;
...
}
}
ECMAScript 运行时 + Host
ECMAScript(简称 ES)是一部由 Ecma International 制定的脚本语言规范,文件编号 ECMA-262。它定义了语言的语法、类型系统、内建对象、错误模型、并发抽象(Promise Jobs)等核心语义,用来保证不同实现之间可以运行同一段脚本代码。
- 1995 年 Brendan Eich 在 Netscape 浏览器中实现了早期的 LiveScript/JavaScript。
- 为了避免与 Sun 的“Java”商标纠纷,1996 年 Netscape 把这门语言送交标准机构 ECMA,双方同意用 “ECMAScript” 作为标准名称,浏览器实现继续对外叫 “JavaScript”。
语言引擎
- 解析 → 生成 AST/IR → 点燃 JIT(Ignition/TurboFan、LLInt/DFG JIT…)。
- 垃圾回收:Oilpan(V8)、Mark-Sweep/Mark-Compact。
Host(浏览器 / Node / WeChat Game Runtime)
- 提供 事件循环队列、DOM、
setTimeout、wx.request…。 - 规范把这些称作 Host APIs;语言层只认
Job Queue与Host*钩子。(ECMAScript 2024 Language Specification, Byby)
伪代码:HostPromiseRejectionTracker
function HostPromiseRejectionTracker(p, op) {
if (op === 'reject') {
queueMacroTask(() => {
if (!p.handled) dispatch('unhandledrejection', {reason: p.result});
});
} else if (op === 'handle') {
p.handled = true;
}
}
微信小游戏 Runtime 在 C++ / Java 层实现 queueMacroTask,把任务压到主线程尾,再经 JSCore/V8 调度(与浏览器概念相同,只是没有 DOM)。
运行时实现
V8 由 Google 于 2008 年为 Chrome 开发,名字源于 “V-8” 引擎,寓意高性能。
- 它同时实现 ECMAScript 和 WebAssembly,被 Node.js、Deno、Electron、微信小游戏(Android)等运行时复用,成为最广泛部署的 ES 引擎之一。
主流 JavaScript 引擎与来源:
| 引擎 | 主要维护方 | 首次发布 | 名称由来 / 定位 |
|---|---|---|---|
| V8 | 2008 | 命名取自 V-8 汽车发动机,强调“高性能” | |
| SpiderMonkey | Mozilla | 1996 | Brendan Eich 首个 JS 实现,被火狐延续至今 |
| JavaScriptCore | Apple / WebKit | 2002 | Safari 内置;别名 Nitro/SquirrelFish;同样实现 ECMAScript |
| Hermes | Meta | 2019 | React Native 专用,面向移动端低内存场景 |
| QuickJS | Fabrice Bellard | 2019 | 超小型、支持最新 ES 特性,常嵌入 IoT |
把四种运行时放在一张脑图里
- 装载器:ClassLoader / .NET MetadataLoader /
PyImport/ ScriptLoader - 执行引擎:解释器 & JIT (+AOT)
- 内存管理:GC(分代、压缩、并行)、引用计数
- 宿主接口:JNI / P-Invoke / C-API /
window.*wx.*
运行时就是“让字节码真正跑起来”的那整套代码与数据区; 编写它意味着:写 Parser/JIT/GC、做平台抽象层、暴露宿主 API,并持续调优内存与性能。理解了这些通用部件,再看任何语言的“runtime”文档就能迅速对号入座。
拓展延申:原生互操作的实现
- 原生互操作(JNI、P/Invoke、C-API、Web IDL…)
以JNI和P/Invoke为例进行说明:
| 步骤 | JNI | P/Invoke |
|---|---|---|
| ① 托管端声明 | native 关键字写进 .class → ClassFile 标志 ACC_NATIVE |
extern + [DllImport] 写进 IL → 方法标志 pinvokeimpl |
| ② 运行时解析 | System.loadLibrary() 触发 动态装载;JVM 用 dlsym/GetProcAddress 找 Java_pkg_Class_meth 或注册表 |
CLR 在 JIT 时查看元数据→调用 LoadLibrary/dlopen & GetProcAddress |
| ③ 桩代码生成 | JVM 按平台 ABI 生成 native stub:• 入栈 JNIEnv* + jobject + 参数• 建立 JNI本地帧 屏蔽 GC |
CLR 生成 IL Stub(.NET 7 起可用 Source Generator,提前产出 C#→CIL):• 参数封送(marshal)• 线程进入 Preemptive GC 模式 |
| ④ 跨界调用 | C 函数里通过 JNIEnv* 表调用 JVM 服务;返回时释放本地引用 |
C 函数直接按 C 调用约定返回;IL Stub 再把结果封送回托管对象 |
| ⑤ 异常/GC 协作 | C 抛异常 ⇒ (*env)->ThrowNew 映射到 Java;GC 不会移动 JNI 本地引用 |
C 侧报错通过 SetLastError,托管层 [DllImport(SetLastError=true)] 捕获;GC 期间 stub 自动 pin/unpin |
0. 设计总览
┌─────────┐ ┌───────────────┐ ┌─────────────┐
│ 元数据 │──┐──▶│ 延迟解析桩 │──┐──▶│ 真正桩 │──▶ 原生函数
└─────────┘ │ └───────────────┘ │ └─────────────┘
│ │
▼ ▼
类加载阶段处理 ACC_NATIVE 首次调用时 patch entry
核心链路: 标记 native → 延迟解析 → 找符号 → 生成/缓存桩 → 线程 & GC 保护 → 参数封送 → 调原生
1. 元数据加载阶段
| 任务 | JNI (HotSpot) | P Invoke (CoreCLR) | 交付物 |
|---|---|---|---|
| 解析“这是 native 方法” | ACC_NATIVE 写进 Method* |
pinvokeimpl 写进 MethodDesc |
修改 Class/Type Loader,支持 native 标志 |
| 保存额外属性 | 无(符号名通过约定推算) | DllImport 属性:库名、调用约定、SetLastError… |
在 运行时元数据表 中新增字段 |
2. 符号解析 & 动态库管理
| 步骤 | JNI | P Invoke | 要点 |
|---|---|---|---|
| 触发时机 | System.loadLibrary() 或第一次调用 |
JIT 初次触发 P Invoke | 都选 懒加载;失败时抛高层异常 |
| 动态装载 | dlopen/LoadLibrary |
同上 | 维护 NativeLibraryCache,库句柄唯一 |
| 符号名规则 | Java_{pkg}_{Class}_{method}__{sig} 替换 / → _,_ → _1 |
函数名直接写在 DllImport |
提供 NameManglingUtil(可配置) |
| Patch 回调地址 | Method*::set_native_function |
MethodDesc::SetNDirectTarget |
确保线程安全,双检 + 锁或 CAS |
3. 桩(Stub)生成器
3.1 生成策略
| 维度 | JNI | P Invoke | 建议 |
|---|---|---|---|
| 生成时机 | 每个 native 方法一次(或 AOT 缓存) | JIT 一次,或 .NET 7 起 Source Gen | 先做 即时生成,后期再做 AOT |
| 目标 | 按平台 ABI & GC 协议包装 | 生成 IL,再由 JIT 编译 | 统一走 StubEmitter:C++ 模板 + MacroAssembler |
| 性能阶梯 | 普通 JNI → Fast JNI → Critical Native | 普通 P Invoke → Stub 内联 → 直接调用 | 暴露“Fast 调用”注解供高级用户 |
3.2 桩内容模板(伪代码)
; prologue
push rbp
mov rbp, rsp
sub rsp, local_size
call ThreadState_EnterNative ; 切换到 native / Preemptive GC
; 参数封送
mov rdx, [rbp+arg0] ; e.g. jint → int32
; ...
; 调用原生符号
call [resolved_addr]
; 返回值封送 / 异常映射
call ThreadState_LeaveNative
mov rsp, rbp
pop rbp
ret
4. 封送 (Marshalling) 引擎
| 类型类别 | JNI API | P Invoke Attribute / API | 关键代码 |
|---|---|---|---|
| 基元 | 直接复制 | 直接复制 | 无 |
| 字符串 | GetStringUTFChars / NewStringUTF |
[MarshalAs(UnmanagedType.LPUTF8Str)] |
StringMarshaller |
| 数组 | GetArrayElements |
fixed / GCHandle pin |
PinnedArrayPool |
| 结构体 | 手写 GetFieldID + byte copy |
StructLayout(LayoutKind.Sequential) |
解析 C# 布局元数据生成拷贝表 |
| 异常 | ThrowNew, ExceptionCheck |
SetLastError=true, Marshal.GetLastWin32Error() |
ErrorMapper |
5. 线程 & GC 协作
| 事项 | JNI | P Invoke | 实现要求 |
|---|---|---|---|
| 线程附加 | AttachCurrentThread / Detach |
自动 | 维护 ThreadLocal JNIEnv / GCMode |
| GC 安全点 | Pin 本地引用;本地帧 | Preemptive vs Cooperative | 在桩的 prologue / epilogue 标记状态 |
| 反向调用 | Call<type>Method |
Delegate → 函数指针 (GCHandle) |
生成 Reverse Stub,确保再次切换 GC 模式 |