内存泄漏:排查了三天,发现是一个delete忘在循环里了
上篇聊了new/delete,有人后台说想看内存泄漏。
来了。这个话题在机器人开发里太重要了,因为机器人程序是长时间运行的——一跑就是几天、几周、甚至几个月。内存泄漏这种问题,在短时间的测试里根本发现不了,等上了产线才爆出来。
讲个真实的故事。
去年帮朋友排查一个bug。他们做AGV的,机器人跑得好好的,大概每跑6个小时就崩一次。重启之后又正常了,再过6小时又崩。
我接过去看,第一反应是内存泄漏。用valgrind跑了一下,果然——每处理一帧激光雷达数据,就泄漏4KB内存。
原因是什么呢?一个同事在解析雷达数据的时候,每帧都new了一个缓冲区,但在某个异常分支里忘了delete。
void processLidarScan(const uint8_t* raw_data, int size) {
uint8_t* buffer = new uint8_t[size];
if (size < MIN_SIZE) {
return; // 这里直接return了,buffer没delete!
}
// 正常处理...
delete[] buffer;
}
size < MIN_SIZE的时候直接return了,buffer没释放。每帧泄漏一次,6小时下来内存就爆了。
修复很简单,加一行delete就行。但这种bug,在代码review的时候很难发现,因为逻辑上是"正确"的——异常数据确实应该提前返回。而且测试的时候,异常数据出现的频率不高,偶尔泄漏一点根本看不出来。
内存泄漏到底是什么?
简单说:你申请了内存,但用完之后没还回去。
操作系统给你的内存是有限的。每次new,你就少一块可用内存。如果一直new不delete,可用内存越来越少,直到系统拒绝分配,程序就崩了。
在机器人开发里,内存泄漏的后果比一般程序更严重。因为机器人程序是7x24小时运行的,内存泄漏是渐进式的——刚开始没事,跑着跑着内存越来越少,最后OOM被系统kill。
更可怕的是,内存泄漏不一定每次都崩。有时候你测试了几个小时都没事,觉得没问题就上线了。结果产线上跑了两天才崩,这时候排查起来就特别痛苦——因为你根本不知道是哪行代码泄漏的。
四种最常见的内存泄漏场景
场景一:提前return,忘了delete。 就是开头那个例子。函数里有多个return路径,某个路径上忘了释放。
void processData() {
int* data = new int[1000];
if (!validate(data)) {
delete[] data; // 这次没忘
return;
}
if (!transform(data)) {
// 忘了delete!
return;
}
delete[] data;
}
场景二:异常抛出,析构没执行。 如果new之后、delete之前抛了异常,delete就不会被执行。
void loadData() {
int* data = new int[1000];
doSomething(); // 这里可能抛异常
delete[] data; // 异常发生时,这行不会执行
}
场景三:容器里的指针没释放。 你往vector里存了裸指针,clear的时候只清了指针,没释放指针指向的对象。
std::vector<Sensor*> sensors;
sensors.push_back(new Lidar());
sensors.push_back(new Camera());
sensors.clear(); // 只清了指针,Lidar和Camera对象没析构!
场景四:基类析构函数不是虚函数。 这是C++特有的坑。如果你通过基类指针delete一个派生类对象,但基类析构不是虚的,派生类的析构函数不会被调用。
class Sensor {
public:
~Sensor() {} // 不是虚函数!
};
class Lidar : public Sensor {
public:
~Lidar() { /* 释放雷达资源 */ }
};
Sensor* s = new Lidar();
delete s; // 只调用了Sensor的析构,Lidar的析构没调用!
怎么检测内存泄漏?
工具一:Valgrind。 Linux下最常用的内存检测工具。
valgrind --leak-check=full ./your_program
它会告诉你哪行代码new了内存但没delete,泄漏了多少字节。缺点是程序会跑得特别慢,大概慢10-30倍。
工具二:AddressSanitizer (ASAN)。 GCC和Clang内置的内存检测工具,编译时加个flag就行。
g++ -fsanitize=address -g your_code.cpp -o your_program
比Valgrind快很多,大概只慢2倍。而且能检测的bug类型更多——除了内存泄漏,还能检测越界访问、use-after-free等。
工具三:Visual Studio内置工具。 Windows下用Visual Studio的调试器,内置了内存诊断功能。可以在运行时查看内存使用情况的变化趋势。
工具四:代码review。 最原始但最有效的方法。每次看到new,就去找对应的delete。如果找不到,就是泄漏。
怎么避免内存泄漏?
方法一:用智能指针。 这是现代C++的标准做法。std::unique_ptr和std::shared_ptr会在离开作用域时自动释放内存。
#include <memory>
void loadData() {
auto data = std::make_unique<int[]>(1000);
// 离开作用域时自动释放,不用手动delete
}
方法二:用容器代替裸数组。 std::vector会自动管理内存,不用你操心。
// 不好
int* data = new int[1000];
// ...
delete[] data;
// 好
std::vector<int> data(1000);
// 离开作用域自动释放
方法三:RAII惯用法。 把资源管理绑定到对象的生命周期上。后面会专门讲。
在机器人开发里,我强烈建议用智能指针和容器。裸的new/delete只在极少数性能敏感的场景下使用。
还有一个实战技巧很多教程没提到:在机器人长运行测试中,你可以用/proc/meminfo或者top命令定时记录进程的内存占用,画一条内存使用趋势图。如果内存曲线是稳定的锯齿形(涨上去又降下来),说明正常;如果是一条缓慢上升的直线,十有八九有泄漏。我们团队之前就是用这个方法,在一个跑了72小时的压力测试里发现了泄漏点,最后定位到一个回调函数里每次都在new一个字符串对象但始终没有释放,日积月累就出了大问题。这种长时间监控的方法虽然笨,但在工程实践中特别管用。我见过不少新人只会用工具扫描,不会手动监控分析,面试时说出来也能体现你的工程素养和排查问题的能力。
给正在准备面试的你一点建议
内存泄漏在面试里经常考,特别是结合项目经验来问。
我建议你做三件事:
第一,知道内存泄漏的常见场景——提前return、异常抛出、容器里的裸指针、基类非虚析构。
第二,会用至少一种检测工具——Valgrind或ASAN。面试的时候说"我用过Valgrind检测内存泄漏",比只说"我知道内存泄漏"有说服力得多。
第三,知道怎么避免——智能指针、容器、RAII。
这里分享一个排查内存泄漏的实战经验:AddressSanitizer(ASAN)比Valgrind更快,适合在开发阶段使用。编译时加上-fsanitize=address -g选项就行,运行时ASAN会自动检测内存泄漏并精确报告泄漏的代码行号。在CI流程里集成ASAN,每次提交代码都自动跑一遍检测,泄漏问题当场就能抓住。而且ASAN只慢2倍左右,Valgrind慢10到20倍,所以ASAN更适合日常高频使用。
面试的时候,如果你能结合项目经验讲一个你排查过的内存泄漏bug,面试官会对你印象深刻。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第13篇 动态内存分配new/delete——堆内存管理的入门 下一篇预告:第15篇 引用的本质——和指针到底什么区别?
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)