你了解内存管理吗?

栈分配和堆分配确实都发生在“对象分配器”的分配阶段, 但它们走的是不同的路径: 栈分配走的是编译期静态分配; 堆分配走的是运行时对象分配器(runtime.mallocgc)。 1.程序启动阶段 Go 运行时启动时(runtime 初始化),会: 向操作系统申请一大块虚拟内存(称为 arena); 由 页分配器(page allocator) 管理这块内存; 构建堆内存管理结构(mheap、mcentral、mcache)。 这部分只是“预留”内存,真正的对象分配还没发生。 2.用户程序触发分配(对象分配阶段) 当用户代码中创建变量时,比如: x := MyStruct{} 编译器会在编译阶段决定这个对象是: 分配在栈上(stack allocation) 还是分配在堆上(heap allocation) 这个决策是通过 逃逸分析(Escape Analysis) 完成的。 3.栈分配的过程 如果编译器认为对象只在当前函数作用域内使用,不会被外部引用: 这个对象会直接分配在栈上; 不会调用运行时的内存分配器; 栈内存是随函数调用帧自动增长/释放的; GC 不需要扫描或回收它。 > 关键:栈分配是编译期确定的,性能最好。 4.堆分配的过程 如果对象被闭包引用、返回地址或传递给其他 goroutine,则会发生逃逸: 编译器在生成代码时,会调用运行时的分配器 runtime.mallocgc; mallocgc 会从当前 P 的 mcache 尝试获取一个合适的 span; 若 mcache 缓存不足,就从 mcentral → mheap 逐层申请; 分配完成后,GC 会在堆上追踪这个对象。 >关键:堆分配是运行时动态完成的,涉及 GC 管理。 5.回收阶段 当对象不再被引用时,GC 会标记并清除; 被清除的内存重新回收到 mcache / mcentral / mheap; 长期未使用的页可能由scavenger(拾荒器)归还给 OS。 6.对比栈和堆分配 类型 分配阶段 分配位置 分配速度 是否由 GC 管理 是否逃逸 栈分配 编译期(静态) 每个 goroutine 的调用栈 极快 否 否 堆分配 运行时(动态) 运行时堆(mheap) 慢 是 是

2025年10月14日 · Mumu

内存对齐(memory alignment)

内存对齐(Memory Alignment)是计算机系统架构和编程中的一个基本概念,它指的是数据在内存中的存储地址必须是某个值的整数倍。这个“某个值”通常是该数据类型的大小或其最大成员的大小(在结构体中)。 通过分析以下具体实例加深内存对齐理解。 type itab struct { inter *interfacetype // 接口类型信息 _type *_type // 实现接口的具体类型信息 hash uint32 // 类型 hash 值 _ [4]byte fun [1]uintptr // 实现接口方法的函数地址 } 一、Go 的结构体内存布局规则 Go 里每个字段在内存中都有一个偏移量(offset),而编译器会自动插入 padding(填充字节),以保证每个字段都按其类型对齐(alignment)。 规则大致是: 每个字段的起始地址必须是该字段类型的对齐倍数。 比如:uint32 对齐要求 4 字节,uintptr(在 64 位机上)对齐要求 8 字节。 整个结构体的大小必须是其内部最大对齐单位的整数倍。 编译器自动插入 padding 字节,但有时源码里会显式加 _ [N]byte 来占位或兼容 ABI。 二、itab 的字段分析(以 64位架构为例) 我们来计算每个字段的内存偏移: 字段 类型 大小 (Size) 自身对齐值 (Align) 偏移量 (Offset)单位:字节 备注 inter *interfacetype 8 字节 8 字节 0 → 8 8 字节对齐 _type *_type 8 字节 8 字节 8→16 8 是 8 的倍数,已对齐 hash uint32 4 字节 4 字节 16→20 16 是 4 的倍数,已对齐 _ [4]byte 4 字节 1 字节 20→24 4 字节的填充 (Padding) fun [1]uintptr 8 字节 8 字节 24→32 8 字节对齐 24 是 8 的倍数 总大小32 字节,结构体最大对齐是 8 字节,总大小 32 是 8 的倍数。 ...

2025年9月5日 · Mumu