1. 内存泄漏基础:什么是内存泄漏、内存泄漏的危害、常见的内存泄漏类型
大家好,我是你们这堂课的老朋友。今天咱们来聊聊内存泄漏——这个让无数程序员头疼的问题。
说实话,我入行头三年,踩过最大的坑就是内存泄漏。有一次线上服务跑了两周,内存从 2GB 涨到 12GB,最后 OOM 被系统杀掉。大半夜被叫起来排查,那滋味,啧,真不好受。
所以这一章,我想把内存泄漏的底裤扒干净。咱们从最基础的概念讲起,一步步深入。
1.1 什么是内存泄漏?
内存泄漏,说白了就是:程序申请了一块内存,用完之后没还给系统。
你想想看,你找图书馆借了本书,看完不还。图书馆的书架就少了一本。时间长了,别人想借也借不到。程序也一样——内存越占越多,系统越来越卡,最后崩了。
用代码来感受一下:
// C++ 示例:典型的内存泄漏
void leak_example() {
int* p = new int(42); // 申请内存
// 忘记 delete p;
// 函数结束,指针 p 销毁,但内存没释放
}
这段代码里,new 出来的 int 对象,永远留在了堆上。没人能再访问它,也没人释放它。这就是泄漏。
Python 里也有类似情况:
# Python 示例:循环引用导致泄漏
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # 互相引用,GC 无法回收
# 即使 del a, del b,内存依然不释放
嗯,这里要注意:Python 有垃圾回收(GC),但循环引用这种场景,GC 不一定能及时处理。我遇到过好几次,就是因为这个,服务内存越跑越高。
1.2 内存泄漏的危害
你可能觉得:「泄漏一点内存而已,多大点事?」
我告诉你,真不是小事。我归纳了四个层面的危害:
| 危害等级 | 具体表现 | 实际影响 |
|---|---|---|
| 轻度 | 内存缓慢增长 | 服务运行几天后变慢,重启后恢复 |
| 中度 | 频繁 GC / 页面交换 | 响应延迟飙升,CPU 占用异常 |
| 重度 | OOM(内存耗尽) | 进程被系统杀死,服务中断 |
| 致命 | 系统级内存耗尽 | 整台机器卡死,影响其他服务 |
我个人习惯把内存泄漏比作「慢性病」。它不像空指针那样立刻崩溃,而是慢慢蚕食你的系统资源。等你发现的时候,往往已经晚了。
1.3 常见的内存泄漏类型
根据我这些年的经验,内存泄漏大致分这么几类。咱们一个一个看:
1.3.1 堆内存泄漏
这是最经典的类型。C/C++ 里 malloc 了没 free,new 了没 delete。Python 里对象引用计数没归零。
// C 语言:堆泄漏
void heap_leak() {
char* buf = (char*)malloc(1024);
// 用完了,但没 free(buf)
// 每次调用泄漏 1KB
}
我曾经在一个嵌入式项目里,发现一个函数每次调用泄漏 256 字节。那个函数每秒被调用 1000 次。你算算,一小时泄漏多少?接近 900MB。嵌入式设备总共才 512MB 内存,半小时就挂了。
1.3.2 资源泄漏
不只是内存会泄漏。文件句柄、网络连接、数据库连接、图形上下文……这些系统资源,用完了不释放,也是泄漏。
// Java 示例:文件句柄泄漏
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("test.txt");
// 读取操作...
// 忘记调用 fis.close()
// 文件句柄一直占着
}
为什么我把这个算作内存泄漏?因为这些资源对象本身也占内存。而且系统对句柄数量有限制,泄漏多了,新的资源申请会失败。
1.3.3 逻辑泄漏
这种最隐蔽。代码没有语法错误,也没有明显的忘记释放。但业务逻辑导致对象一直存活。
举个例子:
// 全局缓存,只增不减
static Map<String, Session> sessionCache = new HashMap<>();
public void onLogin(String userId) {
Session s = new Session(userId);
sessionCache.put(userId, s); // 永远不删除
}
用户登录一次,就往缓存里加一个 Session。用户退出登录呢?没处理。时间长了,缓存里全是僵尸 Session。
1.3.4 回调/监听器泄漏
注册了事件监听器,但忘记注销。对象本该被回收,但因为监听器持有引用,GC 无法处理。
// Android 示例:匿名内部类持有外部引用
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 匿名内部类隐式持有 MyActivity 的引用
findViewById(R.id.button).setOnClickListener(v -> {
// 处理点击
});
}
// Activity 销毁时,如果监听器没注销,Activity 无法被回收
}
我记得有一次排查 Android 应用的内存泄漏,发现一个 Activity 被创建了 50 次,但只销毁了 3 次。剩下的 47 个全被监听器「绑架」了。用户切个屏,内存就涨一截。
1.3.5 静态集合泄漏
把对象放到 static 的 List 或 Map 里。静态变量的生命周期和进程一样长。放进去的对象,永远别想被回收。
// 经典反模式
public class GlobalData {
public static List<byte[]> allData = new ArrayList<>();
public static void addData(byte[] data) {
allData.add(data); // 越加越多,永不释放
}
}
这种代码我见过太多次了。新手图方便,把数据往静态集合里一塞。结果呢?内存泄漏到天荒地老。
1.4 本章小结
好了,咱们把内存泄漏的底细摸清楚了:
- 什么是泄漏:申请了没释放,内存有去无回
- 危害有多大:从变慢到崩溃,从进程到整机
- 常见类型:堆泄漏、资源泄漏、逻辑泄漏、回调泄漏、静态集合泄漏
我个人觉得,理解这些基础概念,比学会用工具更重要。因为工具只能告诉你「哪里泄漏了」,但只有你理解了原理,才能知道「为什么泄漏」和「怎么避免」。
下一章,咱们会深入讲内存分配和释放的底层机制。到时候我会用一张自己画的流程图,把整个过程串起来。嗯,先卖个关子。
核心一句话: 内存泄漏的本质,就是「该回收的没回收」。记住这句话,后面所有内容都围绕它展开。
可可精品资料库