# 查看内存使用
free -h
top
htop
# 查看进程内存详情
pmap -x <pid>
cat /proc/<pid>/status | grep -i vm
# Go 程序内存分析
go tool pprof http://localhost:6060/debug/pprof/heap内存问题特征:
- 内存使用持续增长
- 频繁 GC(Go)或 OOM(Out of Memory)
- Swap 使用率高
- RSS(常驻内存)过大
原因 1:Goroutine 泄漏
// ❌ Goroutine 泄漏
func badHandler(ctx context.Context) {
ch := make(chan int)
go func() {
for {
select {
case <-ch:
// 处理
// 缺少 ctx.Done() 退出机制
}
}
}() // Goroutine 永远不会退出
}
// ✅ 正确:带超时控制
func goodHandler(ctx context.Context) {
ch := make(chan int)
go func() {
for {
select {
case <-ch:
// 处理
case <-ctx.Done():
return // 正确退出
}
}
}()
}原因 2:未关闭的资源
// ❌ 未关闭文件
func badReadFile(filename string) ([]byte, error) {
file, _ := os.Open(filename)
// 缺少 defer file.Close()
return io.ReadAll(file)
}
// ✅ 正确:关闭资源
func goodReadFile(filename string) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close() // 确保关闭
return io.ReadAll(file)
}原因 3:全局变量缓存无限增长
// ❌ 无限增长的缓存
var globalCache = make(map[string]interface{})
func badCache(key string, value interface{}) {
globalCache[key] = value // 永不清理
}
// ✅ 使用 LRU 限制大小
import "github.com/hashicorp/golang-lru"
var lruCache, _ = lru.New(1000) // 最多 1000 个元素
func goodCache(key string, value interface{}) {
lruCache.Add(key, value) // 自动淘汰旧数据
}import (
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
// 启用 pprof
go http.ListenAndServe("localhost:6060", nil)
// 定期打印内存统计
go func() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Alloc=%v MB, TotalAlloc=%v MB, Sys=%v MB, NumGC=%v",
m.Alloc/1024/1024,
m.TotalAlloc/1024/1024,
m.Sys/1024/1024,
m.NumGC)
}
}()
// 业务代码...
}使用 pprof 分析:
# 采集当前堆内存
go tool pprof http://localhost:6060/debug/pprof/heap
# 对比两次内存快照(找泄漏)
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# 运行一段时间后
curl http://localhost:6060/debug/pprof/heap > heap2.prof
# 对比分析
go tool pprof -base heap1.prof heap2.prof
# 查看增长最多的函数
(pprof) top
(pprof) list <function_name># 编译时添加调试信息
gcc -g -o myapp myapp.c
# 使用 Valgrind 检测
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# 输出示例:
# ==12345== LEAK SUMMARY:
# ==12345== definitely lost: 4,096 bytes in 1 blocks
# ==12345== indirectly lost: 0 bytes in 0 blocks# 编译时启用 ASan
gcc -fsanitize=address -g -o myapp myapp.c
# 运行程序,自动检测内存错误
./myappimport "sync"
// ✅ 对象池减少内存分配
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 4096)
},
}
func processRequest(data []byte) {
// 从池中获取
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf) // 归还池中
// 使用 buf 处理数据
copy(buf, data)
// ...
}性能对比:
- 每次分配:100000 次/秒,1GB 内存分配
- 对象池复用:1000000 次/秒,10MB 内存分配
- 性能提升:10x,内存减少:100x
// ❌ 动态扩容(多次分配)
func badAppend(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i) // 多次扩容
}
return result
}
// ✅ 预分配容量(一次分配)
func goodAppend(n int) []int {
result := make([]int, 0, n) // 预分配
for i := 0; i < n; i++ {
result = append(result, i) // 无需扩容
}
return result
}性能对比(n=10000):
- 动态扩容:15 次扩容,500μs
- 预分配:0 次扩容,100μs
- 性能提升:5x
// ❌ 逃逸到堆(需要 GC)
func badAlloc() *int {
x := 42
return &x // 逃逸分析:x 逃逸到堆
}
// ✅ 栈分配(无需 GC)
func goodAlloc() int {
x := 42
return x // x 在栈上
}
// 查看逃逸分析
// go build -gcflags="-m" main.go逃逸分析优化技巧:
// ✅ 避免返回指针
func process() int {
// 值返回,栈分配
return calculate()
}
// ✅ 使用值接收者
type Counter struct {
count int
}
func (c Counter) Inc() Counter { // 值接收者
c.count++
return c
}
// ✅ 避免接口类型(可能逃逸)
func processValue(v int) { // 具体类型
// ...
}import (
"strings"
"unsafe"
)
// ❌ 大量字符串拼接(多次分配)
func concatSlow(strs []string) string {
result := ""
for _, s := range strs {
result += s // 每次创建新字符串
}
return result
}
// ✅ strings.Builder(一次分配)
func concatFast(strs []string) string {
var builder strings.Builder
totalLen := 0
for _, s := range strs {
totalLen += len(s)
}
builder.Grow(totalLen) // 预分配
for _, s := range strs {
builder.WriteString(s)
}
return builder.String()
}
// ✅ 零拷贝转换(不安全,谨慎使用)
func stringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
func bytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}// ❌ 内存浪费(24 字节)
type BadStruct struct {
a bool // 1 字节 + 7 字节 padding
b int64 // 8 字节
c bool // 1 字节 + 7 字节 padding
}
// ✅ 优化对齐(16 字节)
type GoodStruct struct {
b int64 // 8 字节
a bool // 1 字节
c bool // 1 字节 + 6 字节 padding
}
// 查看结构体大小
// unsafe.Sizeof(BadStruct{}) // 24
// unsafe.Sizeof(GoodStruct{}) // 16自动检测工具:
# 安装 fieldalignment
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
# 检测并修复
fieldalignment -fix ./...import "sync"
// ✅ 固定大小内存池
type MemoryPool struct {
pools map[int]*sync.Pool
}
func NewMemoryPool() *MemoryPool {
pools := make(map[int]*sync.Pool)
// 预定义常用大小:1KB, 4KB, 64KB, 1MB
sizes := []int{1024, 4096, 65536, 1048576}
for _, size := range sizes {
s := size // 避免闭包问题
pools[s] = &sync.Pool{
New: func() interface{} {
return make([]byte, s)
},
}
}
return &MemoryPool{pools: pools}
}
func (mp *MemoryPool) Get(size int) []byte {
// 找到最接近的大小
poolSize := mp.findPoolSize(size)
if pool, ok := mp.pools[poolSize]; ok {
return pool.Get().([]byte)[:size]
}
return make([]byte, size) // 回退到直接分配
}
func (mp *MemoryPool) Put(buf []byte) {
poolSize := cap(buf)
if pool, ok := mp.pools[poolSize]; ok {
pool.Put(buf[:cap(buf)])
}
}
func (mp *MemoryPool) findPoolSize(size int) int {
for _, s := range []int{1024, 4096, 65536, 1048576} {
if size <= s {
return s
}
}
return size
}import "runtime/debug"
// ✅ 调整 GC 百分比
func tuneGC() {
// 默认 GOGC=100(堆增长 100% 触发 GC)
// 设置 GOGC=200(减少 GC 频率,增加内存使用)
debug.SetGCPercent(200)
// 或使用环境变量
// export GOGC=200
}
// ✅ 手动触发 GC
func manualGC() {
runtime.GC() // 适用于低峰期清理
}
// ✅ 关闭 GC(特殊场景)
func disableGC() {
debug.SetGCPercent(-1) // 禁用自动 GC
// 在合适时机手动 GC
defer runtime.GC()
}// ✅ 复用对象减少 GC
var requestPool = sync.Pool{
New: func() interface{} {
return &Request{}
},
}
func handleRequest() {
req := requestPool.Get().(*Request)
defer requestPool.Put(req)
// 重置对象状态
req.Reset()
// 处理请求
// ...
}
type Request struct {
data []byte
}
func (r *Request) Reset() {
r.data = r.data[:0] // 清空但保留容量
}import (
"runtime"
"time"
)
// ✅ 监控 GC 性能
func monitorGC() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
var lastNumGC uint32
var lastPauseTotal time.Duration
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
numGC := m.NumGC - lastNumGC
pauseTotal := time.Duration(m.PauseTotalNs) - lastPauseTotal
if numGC > 0 {
avgPause := pauseTotal / time.Duration(numGC)
log.Printf("GC: count=%d, avgPause=%v, heapAlloc=%v MB",
numGC, avgPause, m.Alloc/1024/1024)
}
lastNumGC = m.NumGC
lastPauseTotal = time.Duration(m.PauseTotalNs)
}
}# 查看大页配置
cat /proc/meminfo | grep Huge
# 配置大页(2MB)
echo 512 > /proc/sys/vm/nr_hugepages # 512 * 2MB = 1GB
# 永久配置
echo "vm.nr_hugepages=512" >> /etc/sysctl.conf
sysctl -p# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 启用 THP
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 禁用 THP(某些数据库推荐)
echo never > /sys/kernel/mm/transparent_hugepage/enabledGo 程序使用大页:
import (
"syscall"
"unsafe"
)
// ✅ 使用大页分配内存(Linux)
func allocHugePage(size int) ([]byte, error) {
const MAP_HUGETLB = 0x40000
data, err := syscall.Mmap(
-1,
0,
size,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS|MAP_HUGETLB,
)
return data, err
}性能对比:
- 普通页(4KB):TLB miss 率 5%
- 大页(2MB):TLB miss 率 0.01%
- 性能提升:10-30%(内存密集型应用)
import (
"bytes"
"compress/gzip"
)
// ✅ Gzip 压缩(节省内存)
func compressData(data []byte) ([]byte, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
_, err := writer.Write(data)
if err != nil {
return nil, err
}
writer.Close()
return buf.Bytes(), nil
}
// ✅ Gzip 解压
func decompressData(data []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer reader.Close()
var buf bytes.Buffer
_, err = buf.ReadFrom(reader)
return buf.Bytes(), err
}压缩比对比:
- JSON 数据:压缩比 5:1
- 日志数据:压缩比 10:1
- 二进制数据:压缩比 2:1
// ❌ 行式存储(内存浪费)
type RowStore struct {
records []struct {
ID int64
Name string
Age int
}
}
// ✅ 列式存储(内存高效)
type ColumnStore struct {
IDs []int64
Names []string
Ages []int
}
// 查询单列时,列式存储只加载需要的列
func (cs *ColumnStore) GetIDs() []int64 {
return cs.IDs // 无需加载 Names 和 Ages
}场景:缓存系统内存从 8GB 降低到 2GB
优化步骤:
# pprof 分析发现大量字符串重复
go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap// ✅ 字符串去重(String Interning)
type StringInterner struct {
mu sync.RWMutex
pool map[string]string
}
func NewStringInterner() *StringInterner {
return &StringInterner{
pool: make(map[string]string),
}
}
func (si *StringInterner) Intern(s string) string {
si.mu.RLock()
if interned, ok := si.pool[s]; ok {
si.mu.RUnlock()
return interned
}
si.mu.RUnlock()
si.mu.Lock()
defer si.mu.Unlock()
// Double-check
if interned, ok := si.pool[s]; ok {
return interned
}
si.pool[s] = s
return s
}
// 使用示例
var interner = NewStringInterner()
type CacheItem struct {
Key string
Value string
}
func addToCache(key, value string) {
item := CacheItem{
Key: interner.Intern(key), // 去重
Value: interner.Intern(value), // 去重
}
cache.Add(item)
}性能对比:
- 优化前:100万条目,8GB 内存(大量重复字符串)
- 优化后:100万条目,2GB 内存(字符串去重)
- 内存减少:75%
场景:处理大文件时内存占用从 10GB 降低到 100MB
import (
"bufio"
"os"
)
// ❌ 一次性加载全部(内存爆炸)
func processBad(filename string) error {
data, _ := os.ReadFile(filename) // 10GB 文件全部加载
for _, line := range strings.Split(string(data), "\n") {
processLine(line)
}
return nil
}
// ✅ 流式处理(固定内存)
func processGood(filename string) error {
file, _ := os.Open(filename)
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 64*1024), 1024*1024) // 1MB 缓冲
for scanner.Scan() {
processLine(scanner.Text()) // 逐行处理
}
return scanner.Err()
}- 是否关闭了所有资源?(文件、连接、定时器)
- Goroutine 是否正确退出?
- 全局缓存是否有大小限制?
- 是否使用了 pprof 分析内存?
- 是否使用了对象池?(sync.Pool)
- 是否预分配了容量?(make)
- 是否避免了不必要的堆分配?(逃逸分析)
- 是否优化了字符串拼接?
- 是否调整了 GOGC 参数?
- 是否复用了对象?
- 是否监控了 GC 指标?
- 是否使用了大页内存?
- 是否使用了数据压缩?
- 是否优化了数据结构布局?
核心要点:
- ✅ 避免泄漏:关闭资源、正确退出 Goroutine、限制缓存大小
- ✅ 复用对象:对象池、预分配容量减少分配
- ✅ 栈优先:避免逃逸到堆,减少 GC 压力
- ✅ GC 调优:调整 GOGC、监控 GC 指标
- ✅ 工具分析:pprof、valgrind 定位问题
优化优先级:
修复泄漏 > 对象复用 > 减少分配 > GC调优 > 高级优化