mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1249 字
3 分钟
驻留(Interning)
2026-06-13

一、为什么需要驻留#

编译器在解析源代码时,会遇到大量重复的标识符。一个 Java 项目中 String 这个词可能出现上万次。如果每次都创建新的字符串对象,内存中就躺着上万个内容相同的 String——每个占 40+ 字节(对象头 + char 数组 + 哈希码),加起来几百 KB 的浪费。

更严重的是性能问题。编译器需要频繁比较标识符是否相同:变量查找、类型检查、作用域解析。每次比较都要逐字符扫描两个字符串,O(n) 的开销。在编译大型项目时,字符串比较可能占总编译时间的 10% 以上。

驻留给出的方案是:每个唯一值只在驻留表中存一份,所有引用指向同一个规范实例。比较两个驻留过的字符串,只需要比较指针或整数 ID,O(1) 搞定。

二、现实类比#

邮局只存储一份每个邮编的副本,给所有人一个引用。不是每封信都带一份「94105」,它们都指向同一个共享条目。查两封信是否寄往同一个地方,比较引用就行,不用逐字比对邮编。

三、核心思想#

驻留将每个唯一值在表中只存储一次,并分发轻量级标识符(符号 ID 或驻留指针)指向规范副本。结构上相等的两个值获得相同的标识符,因此相等性检查变成 O(1) 的指针/整数比较。

flowchart LR A["intern('hello')"] --> P[符号表] B["intern('hello')"] --> P C["intern('world')"] --> P P -->|"ID: 0"| H["'hello'"] P -->|"ID: 0(复用)"| H P -->|"ID: 1"| W["'world'"] text intern("hello") → 0 intern("world") → 1 intern("hello") → 0 (复用!) ┌───────────────────────┐ │ Symbol Table │ ├────┬──────────────────┤ │ ID │ Value │ ├────┼──────────────────┤ │ 0 │ "hello" │ │ 1 │ "world" │ │ 2 │ "foo" │ └────┴──────────────────┘ 相等性检查:symbol_a == symbol_b (整数比较, O(1)) 而非:strcmp(str_a, str_b) (逐字符扫描, O(n))
属性说明
intern()首次 O(n),命中时 O(1) 摊销哈希查找 + 存储
相等性O(1)整数/指针比较
内存去重每个唯一值只存储一次
权衡驻留表单调增长值永不释放,需要淘汰策略

驻留表的增长问题:经典驻留表只增不减。编译器处理 10,000 个源文件后,驻留器可能持有 200 万个字符串,占用 500MB——其中大量是一次性标识符,再也不会被引用。生产级驻留器需要作用域策略:按编译单元清除、使用弱引用或只对高频重复的标识符驻留。

四、变体与对比#

模式与驻留的关系适用场景
享元驻留是实现享元的机制——去重相同的不可变值享元是设计模式,驻留是实现手段
LRU 缓存驻留表可以使用 LRU 淘汰来限制内存使用内存受限的驻留场景
布隆过滤器布隆过滤器可以在昂贵的驻留表查找前做预检查减少驻留表查找次数
写时复制驻留共享完全不可变的值;CoW 共享直到修改驻留适合永不修改的值

五、多语言实现#

5.1 Go 实现#

package interning
import "sync"
// Interner 字符串驻留器,返回整数 ID 作为符号
type Interner struct {
mu sync.RWMutex
strToID map[string]int
idToStr []string
}
func NewInterner() *Interner {
return &Interner{
strToID: make(map[string]int),
}
}
// Intern 驻留字符串,返回符号 ID
// 相同字符串始终返回相同 ID
func (it *Interner) Intern(s string) int {
it.mu.RLock()
if id, ok := it.strToID[s]; ok {
it.mu.RUnlock()
return id // 命中:返回已有 ID
}
it.mu.RUnlock()
it.mu.Lock()
defer it.mu.Unlock()
// 双重检查
if id, ok := it.strToID[s]; ok {
return id
}
id := len(it.idToStr)
it.strToID[s] = id
it.idToStr = append(it.idToStr, s)
return id
}
// Resolve 通过 ID 反查原始字符串
func (it *Interner) Resolve(id int) (string, bool) {
it.mu.RLock()
defer it.mu.RUnlock()
if id < 0 || id >= len(it.idToStr) {
return "", false
}
return it.idToStr[id], true
}
// Size 返回驻留的唯一字符串数量
func (it *Interner) Size() int {
it.mu.RLock()
defer it.mu.RUnlock()
return len(it.idToStr)
}

使用示例——编译器标识符比较:

func compile(sources []string) {
interner := interning.NewInterner()
// 所有标识符通过驻留比较
id1 := interner.Intern("String")
id2 := interner.Intern("String")
// 比较两个标识符:整数比较 O(1)
if id1 == id2 {
// 同一个符号,无需逐字符比较
}
}

5.2 TypeScript 实现#

class StringInterner {
private strToId = new Map<string, number>();
private idToStr: string[] = [];
// 驻留字符串,返回符号 ID
intern(s: string): number {
const existing = this.strToId.get(s);
if (existing !== undefined) return existing;
const id = this.idToStr.length;
this.strToId.set(s, id);
this.idToStr.push(s);
return id;
}
// 通过 ID 反查字符串
resolve(id: number): string | undefined {
return this.idToStr[id];
}
get size(): number {
return this.idToStr.length;
}
}
// 使用示例:JSON 解析器字段名去重
const fieldInterner = new StringInterner();
function parseObject(keys: string[]): Map<number, unknown> {
const result = new Map<number, unknown>();
for (const key of keys) {
// 重复的字段名(如 "name"、"age")只存储一份
const symbolId = fieldInterner.intern(key);
result.set(symbolId, null);
}
return result;
}

六、生产验证#

项目源码位置用途
Rust 编译器 rustcsymbol.rs#L24-L79Symbolu32 的 newtype——全局驻留表的索引。Rust 编译器中所有标识符都是 Symbol,相等性是一次 u32 比较
CPythonunicodeobject.c#L14416-L14472PyUnicode_InternInPlace 通过将字符串存储在全局 dict 中来驻留。所有标识符字符串自动驻留以实现 O(1) 的 dict 查找
V8 引擎Internalized StringsV8 驻留用作属性名的字符串,实现 O(1) 属性查找

七、小结#

何时使用:

  • 编译器和解释器——驻留标识符、关键字和类型描述符以实现快速相等性检查
  • 数据库查询引擎——驻留列名、表名以加速查询规划中的比较
  • 序列化——驻留 JSON/XML 中重复的字段名以减少内存
  • 字符串密集型工作负载——需要比较相同字符串数百万次的系统

何时不用:

  • 短生命周期字符串——字符串被快速创建和丢弃,驻留开销大于收益
  • 大多数字符串唯一——很少有重复字符串时,驻留表浪费内存而不节省比较
  • 有清理需求的内存受限场景——经典驻留表单调增长,考虑弱引用或 LRU 淘汰
  • 用户控制的输入——驻留外部输入可能导致驻留表无限增长(DoS 风险)

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

驻留(Interning)
https://blog.souloss.com/posts/programming/memory/memory-interning/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时