mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1109 字
3 分钟
程序员应当知道的内存知识
2020-09-25

一、程序员应当知道的内存知识#

作为后端程序员,理解内存的工作原理对写出高性能代码至关重要。本篇从堆与栈的区别出发,延伸到内存分配器的实现,最后讨论大内存服务的调优实践。

二、堆与栈的区别#

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 管理内存。

分配策略:

  1. 快速分配:小内存(<= 128KB)从线程本地缓存(arena)分配
  2. 慢速分配:大内存向系统(brk/mmap)申请
  3. 合并:相邻的空闲块合并减少碎片

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 allocator
2. 小对象(16B - 32KB):使用 mcache 中的 mspan
3. 大对象(> 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. 闭包捕获大对象#

// 泄漏:闭包捕获了整个 largeSlice
largeSlice := 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 后未 free
void leak() {
char *p = malloc(1024);
// 使用 p
// 忘记 free(p)
}
// 正确:配对使用
void noLeak() {
char *p = malloc(1024);
if (p == NULL) return;
// 使用 p
free(p); // 必须释放
}

4.3 检测工具#

  • Go: pprofgo test -memprofile
  • C/C++: valgrind --leak-check=full

五、GC 压力优化#

Go 的 GC 会暂停应用(Stop The World),大内存服务的 GC 压力尤为明显。

5.1 优化思路#

1. 减少分配次数#

// 减少分配
// 不好:每次创建新 slice
for 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. 内存持续增长#

排查步骤:

  1. pprof 分析堆内存
  2. 检查是否有未退出的 goroutine
  3. 检查是否有未清理的 map 引用

2. GC 延迟高#

解决方向:

  1. 降低 GOGC 值,增加内存换时间
  2. 使用 sync.Pool 复用大对象
  3. 考虑 Go 1.21+ 的 WCSS(Write Barrier Stacks)

6.3 监控指标#

  • go_memstats_heap_alloc_bytes:已分配的堆内存
  • go_gc_duration_seconds:GC 耗时
  • go_goroutines:活跃 goroutine 数量

七、参考#


参考#

支持与分享

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

程序员应当知道的内存知识
https://blog.souloss.com/posts/programming/programming-memory/
作者
Souloss
发布于
2020-09-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时