1109 字
3 分钟
程序员应当知道的内存知识
一、程序员应当知道的内存知识
作为后端程序员,理解内存的工作原理对写出高性能代码至关重要。本篇从堆与栈的区别出发,延伸到内存分配器的实现,最后讨论大内存服务的调优实践。
二、堆与栈的区别
2.1 栈(Stack)
栈是编译器自动管理的内存区域,用于存储函数的局部变量和函数调用上下文。
func foo() { a := 10 // 栈上分配 b := "hello" // 栈上分配 bar(a, b)}栈的特点:
- 自动管理:函数返回时自动释放
- 空间连续:访问效率高,缓存友好
- 大小有限:通常只有几 MB,过深递归会导致栈溢出
- 快速分配:仅移动栈指针,无需查找
2.2 堆(Heap)
堆是程序运行时申请的动态内存区域,需要手动管理(分配和释放)。
func foo() { // 堆上分配 a := make([]int, 1024) // 切片底层数组在堆上 b := new(int) // new 分配在堆上 s := &Student{} // 结构体指针在堆上}堆的特点:
- 手动管理:分配用
make/new,释放依赖 GC(Go)或手动 free(C) - 空间灵活:可以分配任意大小
- 碎片化:频繁分配和释放会导致内存碎片
- 分配昂贵:需要查找足够大的连续空间
2.3 栈与堆的选择
Go 语言中:
- 基本类型(int、float、bool)和指针通常在栈上
make()、new()、切片、map、chan、接口类型在堆上- 闭包捕获的变量在堆上
func foo() { x := 10 // 栈上 y := make([]int, 10) // 堆上(切片底层数组) z := new(int) // 堆上}三、内存分配器
3.1 glibc 的 ptmalloc
Linux 默认的内存分配器是 ptmalloc2,通过 malloc/free 管理内存。
分配策略:
- 快速分配:小内存(<= 128KB)从线程本地缓存(arena)分配
- 慢速分配:大内存向系统(brk/mmap)申请
- 合并:相邻的空闲块合并减少碎片
3.2 tcmalloc(Google)
tcmalloc(Thread-Caching Malloc)是 Go 语言和许多高性能服务采用的分配器。
核心设计:
- 线程本地缓存(Thread Cache):每个线程有自己的小内存缓存,避免锁竞争
- 中心分配器:线程本地缓存不够时向中心分配器申请
- 大小分级:将内存按大小分级(class),每级有专门的分配算法
线程本地缓存 → 中心分配器 → 系统内存 (无锁) (有锁)tcmalloc 的小对象分配(<= 32KB)非常快,通常在纳秒级别。
3.3 jemalloc(Facebook)
jemalloc 是 Firefox 和许多数据库采用的分配器,与 tcmalloc 类似但有不同:
- 更好的内存碎片管理
- 明细的统计信息:
malloc_stats_print()可输出详细内存使用报告 - size class 划分更细:分配精度高
3.4 Go 的内存分配器
Go 1.5 之后使用自研的内存分配器,基于 tcmalloc 思想:
Go 内存分配:1. 微对象(< 16B):使用 mcache 的 tiny allocator2. 小对象(16B - 32KB):使用 mcache 中的 mspan3. 大对象(> 32KB):直接向 mheap 申请Go 的分配器有三层:
- mcache:每个 P(Processor)的本地缓存,无锁
- mcentral:全局共享,锁保护
- mheap:向系统申请内存
四、内存泄漏的常见原因
4.1 Go 中的内存泄漏
Go 有 GC,但内存泄漏仍然可能发生:
1. 未释放的协程
// 泄漏:goroutine 永远不会退出func leak() { go func() { for { time.Sleep(time.Second) } }()}
// 正确:使用 context 控制生命周期func noLeak(ctx context.Context) { go func() { for { select { case <-ctx.Done(): return // 退出 default: time.Sleep(time.Second) } } }()}2. 闭包捕获大对象
// 泄漏:闭包捕获了整个 largeSlicelargeSlice := make([]int, 1e8)go func() { for _, v := range largeSlice { process(v) }}()
// 正确:只传递需要的数据go func(data []int) { for _, v := range data { process(v) }}(largeSlice[:100]) // 只传递需要的部分3. 未清理的 map 引用
// 持续向 map 写入但不清理m := make(map[string]interface{})for { m[generateKey()] = largeObject // 定期 m = make(map[string]interface{}) 重置}4.2 C/C++ 中的内存泄漏
// 泄漏:malloc 后未 freevoid leak() { char *p = malloc(1024); // 使用 p // 忘记 free(p)}
// 正确:配对使用void noLeak() { char *p = malloc(1024); if (p == NULL) return; // 使用 p free(p); // 必须释放}4.3 检测工具
- Go:
pprof、go test -memprofile - C/C++:
valgrind --leak-check=full
五、GC 压力优化
Go 的 GC 会暂停应用(Stop The World),大内存服务的 GC 压力尤为明显。
5.1 优化思路
1. 减少分配次数
// 减少分配// 不好:每次创建新 slicefor i := 0; i < n; i++ { result = append(result, process(i))}
// 好:预分配容量result := make([]Result, 0, n)for i := 0; i < n; i++ { result = append(result, process(i))}2. 对象池复用
// sync.Pool 保存临时对象var bufferPool = sync.Pool{ New: func() interface{} { return &bytes.Buffer{} },}
func usePool() { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }() // 使用 buf}3. 减少指针使用
GC 对指针的处理比值类型更慢。在高 GC 压力场景,可以用值类型替代指针:
// 指针:GC 需要追踪type Node *struct{ value int }
// 值:GC 开销小type Node struct{ value int }4. 控制 GC 频率
# 设置 GC 目标百分比(默认 100%,降低此值会减少 GC 频率但会增加内存使用)GOGC=200 ./server
# 或在代码中设置debug.SetGCPercent(200)六、大内存服务的调优实践
6.1 估算内存使用
内存总量 ≈ 活跃对象 + 缓存 + 空闲内存 + 元数据
- 活跃对象:正常业务数据- 缓存:可回收(GC 后释放)- 空闲内存:已分配但未使用- 元数据:堆和运行时数据结构开销6.2 常见问题与解决
1. 内存持续增长
排查步骤:
pprof分析堆内存- 检查是否有未退出的 goroutine
- 检查是否有未清理的 map 引用
2. GC 延迟高
解决方向:
- 降低 GOGC 值,增加内存换时间
- 使用
sync.Pool复用大对象 - 考虑 Go 1.21+ 的 WCSS(Write Barrier Stacks)
6.3 监控指标
go_memstats_heap_alloc_bytes:已分配的堆内存go_gc_duration_seconds:GC 耗时go_goroutines:活跃 goroutine 数量
七、参考
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
Go 程序的启动
golang 从汇编入口 `rt0_linux_amd64.s` 出发,追踪 Go 程序启动的完整调用链——runtime 初始化、goroutine 调度器初始化、全局变量初始化,直到用户 main 函数被调用的每一个关键节点。
2
Go 内存管理深度解析
golang 深入解析 Go 内存管理——分级分配器、TCMalloc 原理、堆内存分配与栈内存分配、逃逸分析、memhash 实现
3
Go 1.22+ for 循环变量语义的革命性变化
golang Go 1.22 彻底解决了 for 循环变量闭包陷阱——详细解析新语义、迁移策略以及对既有代码的影响。
4
Go 性能优化实战
golang Go 性能优化指南——pprof CPU/内存分析、逃逸分析、GC 调优、sync.Pool 复用、字符串拼接优化、切片预分配
5
Go GC 机制深度解析
golang 深入解析 Go 垃圾回收机制——从 GC 触发条件到四个 GC 阶段(sweep termination、并发 mark、mark termination、sweep),结合 Go runtime 源码讲解三色标记法、写屏障与 GOGC 调优参数。






