内存泄漏基础:从一次线上事故说起

我记得那是几年前的一个深夜。凌晨两点,监控告警突然炸了——某核心服务的内存占用从正常的4G一路飙到了12G,眼看就要OOM。我登录上去一看,进程的RSS还在以每分钟200M的速度上涨。当时我后背就凉了。

嗯,这就是典型的内存泄漏。说白了,就是程序申请了内存,用完之后没还回去。一次两次没事,但日积月累,系统迟早被拖垮。

什么是内存泄漏?

内存泄漏,专业点说:堆内存被分配后,程序失去了对它的所有引用,导致这块内存既无法被使用,也无法被释放。

我习惯用一个比喻来理解:你去图书馆借了一本书,看完后随手扔在某个角落,既不放回书架,也不办理归还。图书馆的系统里,这本书的状态永远是「已借出」。时间一长,图书馆的书全被你「借」走了,新读者来了无书可借——这就是内存泄漏。

在C/C++里,你手动malloc/new了一块内存,但忘记free/delete了。在Python里,你创建了一个全局列表不断往里塞对象,但从不清理。这两种情况,都是内存泄漏。

核心定义:内存泄漏 = 分配内存 + 丢失引用 + 无法回收

内存泄漏的危害

你可能觉得,泄漏一点内存没什么大不了的。我在项目中见过太多这种想法了,结果呢?

  • 性能下降:内存越占越多,操作系统开始频繁swap,程序响应越来越慢。你想想看,一个本该毫秒级响应的接口,突然变成几秒钟——用户不骂娘才怪。
  • 系统崩溃:内存耗尽后,malloc返回NULL,new抛出std::bad_alloc。程序直接挂掉。更可怕的是,OOM killer会随机杀掉进程,可能杀的不是你的服务,而是隔壁的守护进程——连锁反应,整个集群都可能瘫痪。
  • 资源浪费:泄漏的内存无法被其他进程复用。你明明有64G内存,实际能用的可能只有40G。云服务器按量计费,这钱花得冤不冤?
  • 安全隐患:泄漏的内存中可能包含敏感数据(密码、密钥、用户信息)。如果这块内存被重新分配给其他进程,数据就泄露了。我曾经排查过一个案例,泄漏的内存里竟然有用户的session token——细思极恐。
危害类型 表现 严重程度
性能下降 响应变慢、CPU升高 中
系统崩溃 OOM、进程被杀 高
资源浪费 内存利用率低 低
安全隐患 敏感数据泄露 极高

常见的内存泄漏类型

搞清楚了危害,咱们来看看实际工作中最常见的几种泄漏类型。我按自己的经验分了四类:

1. 堆内存泄漏(Heap Leak)

这是最经典的一种。C/C++里malloc/free、new/delete没配对。Python里对象引用计数没归零,GC回收不了。

// C++ 示例:典型的堆内存泄漏
void leak_function() {
    int* p = new int[100];  // 分配了400字节
    // ... 做了一些操作
    // 忘记 delete[] p;
    return;  // p 丢失了引用,内存泄漏
}

我刚开始写C++时也犯过这种错。后来养成了一个习惯:每次写new,立刻补上delete。就像开门后马上把钥匙挂回挂钩上一样。

2. 资源泄漏(Resource Leak)

不只是内存会泄漏。文件句柄、socket连接、数据库连接、锁——这些系统资源用完后不释放,同样会出问题。

// Python 示例:文件句柄泄漏
def read_config():
    f = open('config.json', 'r')  # 打开了文件
    data = f.read()
    # 忘记 f.close()
    return data  # 文件句柄泄漏了

Python里我推荐用with语句,自动帮你释放资源。C++里RAII(资源获取即初始化)是解决这类问题的利器——说白了,就是让对象的析构函数自动释放资源。

避坑指南:我曾经排查过一个socket泄漏的案例。服务端每接受一个连接就创建一个线程,但线程退出时没有close socket。结果连接数到了1024(ulimit默认值),新连接全部被拒绝。排查了整整两天才找到原因——嗯,从那以后我写网络程序第一件事就是检查资源释放路径。

3. 逻辑泄漏(Logical Leak)

这种最隐蔽。程序没有忘记释放内存,但逻辑上让某些对象永远无法被回收。

// Python 示例:全局缓存导致逻辑泄漏
cache = []

def process_request(data):
    result = expensive_computation(data)
    cache.append(result)  // 缓存越来越大,从不清理
    return result

你想想看,这个cache列表会无限增长。虽然每个对象都有引用(被列表持有),但程序逻辑上永远不会删除它们。这就是逻辑泄漏——内存没丢,但永远用不上。

我见过一个真实案例:某日志系统把每次请求的完整报文都缓存到内存里,说是为了「后续分析」。结果跑了三天,内存爆了。分析个屁,服务都挂了。

4. 隐式泄漏(Implicit Leak)

这类泄漏跟语言特性有关。比如C++的虚析构函数问题:

// C++ 示例:基类析构函数不是虚函数
class Base {
public:
    ~Base() {}  // 非虚析构
};

class Derived : public Base {
    int* data;
public:
    Derived() { data = new int[1000]; }
    ~Derived() { delete[] data; }  // 永远不会被调用
};

Base* p = new Derived();
delete p;  // 只调用了Base的析构,Derived的析构没调用!
// data 泄漏了

Python里也有类似问题:循环引用导致GC无法回收。两个对象互相引用,引用计数永远不为0。

注意:Python虽然有垃圾回收,但循环引用+自定义__del__方法会导致对象无法被回收。我建议:能用弱引用(weakref)的地方尽量用弱引用,别让对象互相「抱死」。

知识体系总览

说了这么多,咱们用一张图来总结一下内存泄漏的核心知识体系:

内存泄漏知识体系 内存泄漏 堆内存泄漏 malloc/free不配对 资源泄漏 句柄/socket未关闭 逻辑泄漏 缓存无限增长 隐式泄漏 虚析构/循环引用 危害:性能下降 → 系统崩溃 → 资源浪费 → 安全隐患

这张图把内存泄漏分成了四大类。我个人觉得,实际工作中80%的泄漏都能归到这四类里。你排查的时候,先想想属于哪一类,方向就对了。

我的经验:遇到内存泄漏,先别急着上工具。花5分钟想想:是堆内存没释放?还是资源没关闭?还是缓存逻辑有问题?很多时候,想清楚类型比盲目用valgrind更高效。

好了,这一章咱们把内存泄漏的基础概念、危害和常见类型都捋了一遍。下一章我会带你深入实战——用具体的工具和方法,一步步定位泄漏点。到时候咱们再细聊。