内存泄漏问题
内存泄漏问题
内存泄漏的工具
sudo apt-get install valgrind
valgrind --leak-check=yes ./GODService /home/Downloads/0826pm/tt7
内存泄漏原理、修复原理与数据采集方法
一、内存泄漏的详细原理
1.1 什么是内存泄漏
内存泄漏的本质是:程序向操作系统申请了内存(堆分配),但在不再需要时没有归还,导致这块内存既无法被程序复用,也无法被操作系统回收。随着运行时间累积,进程占用的物理内存(RSS)持续增长,最终触发 OOM(Out-Of-Memory)被系统杀死。
在嵌入式 Linux(ARM Cortex A55,通常 512MB~2GB 内存)上,这个问题尤为致命——你的车机挂机半小时就崩溃,正是因为多个服务同时泄漏,系统内存耗尽。
1.2 五个泄漏点的底层原理
泄漏点 1:裸指针无拷贝控制 → double-free / 泄漏
原理:C++ 中用 new 分配的对象,必须用 delete 释放。如果类持有裸指针成员,但没有正确实现"拷贝构造函数"和"拷贝赋值运算符"(即违反 Rule of Three/Five),会出现两种灾难:
场景 A(double-free):
对象 obj1 持有指针 p → new 了对象 X
对象 obj2 = obj1(浅拷贝)→ obj2.p 也指向 X
obj1 析构 → delete p → X 被释放
obj2 析构 → delete p → 同一块内存被第二次释放 → 堆损坏 / 崩溃
场景 B(泄漏):
对象 obj1 持有指针 p → new 了对象 X
obj1 = obj2(赋值)→ p 被覆盖指向 Y,但原来的 X 没有 delete → X 永远泄漏
你的 StixelWorld 类中:
FreeSpaceDoubao* fsd; // 裸指针
HeightSegmentationDoubao* heightsegmentdb; // 裸指针
// 构造函数 new,析构函数 delete,但没有禁用拷贝
虽然当前代码中 StixelWorld 被 shared_ptr 包裹(StixelDetect.hpp 中 std::shared_ptr<StixelWorld> stixelworld_),暂时不会发生拷贝,但这是一颗定时炸弹——任何后续维护者如果不小心按值传递或赋值,就会触发 double-free。更关键的是,裸指针在异常发生时(如构造函数中第二个 new 抛出异常),第一个已分配的对象无法被自动释放。
泄漏点 2:每帧创建大临时对象 → 内存碎片
原理:这不是严格意义上的"泄漏"(内存最终会被释放),但效果与泄漏相同——进程 RSS 持续增长且不回落。这是嵌入式平台上最常见、也最容易被忽视的问题。
时间线(每帧 100ms,即每秒 10 帧):
t=0ms: cv::Mat1f columns(128, 360) → 申请 184,320 字节(184KB)
t=50ms: columns 使用完毕,函数返回 → 释放 184KB
t=100ms: 下一帧,再次申请 184KB → 但上一块可能还没被"归还"给 OS
t=150ms: 再次释放...
...
为什么会碎片? glibc 的 malloc 使用 ptmalloc 分配器,它的工作机制是:
-
小块内存(< 128KB)通过
brk()从堆顶分配,释放后不会立即归还操作系统,而是留在进程的 free list 中供下次复用 -
但如果多次申请/释放的大小不一致,或者中间有其他分配"插桩",free list 中会留下大量无法合并的空洞
-
你的
columns是 184KB(>128KB),走mmap分配路径——mmap的内存释放后理论上会归还 OS,但频繁mmap/munmap会导致 虚拟地址空间碎片 和 缺页中断开销
更严重的是,你的 compute() 函数中同时还有:
-
std::vector<float> roadDisp(vmax)— 每帧创建 -
std::vector<int> lowerPathDb(columns.rows)— 每帧创建 -
std::vector<int> upperPath_Doubao(columns.rows)— 每帧创建 -
PCL 点云
cloud.makeShared()— 每帧创建两次
这些分配/释放交织在一起,分配器很难高效回收,最终表现为 RSS 只增不减。
泄漏点 3:失败路径忘记 delete → 确定性泄漏
原理:这是最经典的 C++ 内存泄漏模式——new 之后,在某个条件分支中没有 delete 就跳出了作用域。
goyu_image_data* tmp = new goyu_image_data; // 申请内存(含多路图片缓冲,约数MB)
if (isGood && (num_image == 2 || num_image == 4)) {
image_paths.push_back(tmp); // ✅ 成功路径:加入 vector,后续会被使用
}
// ❌ 失败路径:isGood=false 或 num_image 不匹配
// tmp 既没有加入 vector,也没有 delete
// 函数结束后 tmp 指针销毁,但指向的内存永远丢失
每个 goyu_image_data 对象内部包含 CameraForRearPictureData,其中有 front_left_pic、front_right_pic、rear_left_pic、rear_right_pic 四路图片数据,每路都有 picture_data 缓冲。单个对象可能占用数 MB。
虽然这个函数只在 AMD 平台的离线回放模式(#if defined(AMD))下调用,但如果回放目录中有损坏的图片文件(isGood=false),每遇到一张坏图就泄漏数 MB,回放几千张图后泄漏量非常可观。
泄漏点 4:重复 makeShared() → 双倍内存 + 碎片
原理:PCL 的 PointCloud::makeShared() 会在堆上创建一个新的 PointCloud 对象并返回 shared_ptr。
// 原代码
tree->setInputCloud(cloud.makeShared()); // 创建 PointCloud A(引用计数=1)
euclidean_cluster->setInputCloud(cloud.makeShared()); // 创建 PointCloud B(引用计数=1)
-
cloud是栈上的pcl::PointCloud<pcl::PointXYZ>对象 -
第一次
makeShared():把cloud的数据深拷贝到堆上的 PointCloud A,A 的引用计数为 1,被tree持有 -
第二次
makeShared():又把cloud的数据深拷贝到堆上的 PointCloud B,B 的引用计数为 1,被euclidean_cluster持有 -
函数结束时,A 和 B 分别被 tree 和 euclidean_cluster 释放(因为它们是成员变量,持有的是上一帧的点云,setInputCloud 会替换旧的)
问题在于:两个点云内容完全相同,却分配了两份内存。如果点云有 5000 个点(每个点 16 字节 = 80KB),每帧就多浪费 80KB。同时,两次独立的堆分配增加了内存碎片的概率。
泄漏点 5:vector capacity 不释放 → 内存持续占用
原理:std::vector 的 clear() 方法只将 size 置为 0,不会释放 capacity(已分配的内存)。
某一帧场景特别复杂(路口、多车多人):
stixels 从 0 增长到 3000 个 → vector 自动扩容,capacity 变为 4096
占用内存:4096 × sizeof(Stixel) ≈ 4096 × 24 = 98KB
后续帧场景简单(直道、无障碍物):
stixels.clear() → size=0,但 capacity 仍为 4096
这 98KB 内存被持续占用,不会归还操作系统
如果 world_pixels(三维 vector)某一帧有 2000 个 stixel:
外层 vector capacity=2048
每个内层 vector<std::vector<float>> 也有自己的 capacity
最内层 vector<float>(3个元素)每个都有独立的堆分配 + allocator overhead
总占用可能达到 2048 × (24 + 24 + 12) ≈ 123KB + 大量堆碎片
std::vector::shrink_to_fit() 理论上可以释放多余 capacity,但 C++ 标准规定这是一个非强制请求(“non-binding request”),实现可以忽略。因此最可靠的方式是 swap 技巧:
std::vector<T>().swap(vec); // 创建临时空 vector,与 vec 交换,临时 vector 析构时释放原内存
你的代码中 world_pixels 和 cluster_stixels 是 public 成员,在 detect() 中被 clear(),在 get_visual() 中被重新填充。如果某一帧 get_visual() 没有被调用(visual_common_result=false),它们就保持空但 capacity 很大的状态,持续占用内存。
二、修复的原理
2.1 RAII 与 unique_ptr —— 用对象生命周期管理内存
核心思想:资源获取即初始化(RAII)——将资源(内存)的生命周期绑定到对象的生命周期上。对象构造时获取资源,对象析构时自动释放资源,无论正常返回还是异常抛出。
// 修复前:手动管理,容易出错
FreeSpaceDoubao* fsd = new FreeSpaceDoubao(); // 获取
// ... 中间可能抛异常 ...
delete fsd; // 释放(如果抛异常就到不了这里)
// 修复后:unique_ptr 自动管理
std::unique_ptr<FreeSpaceDoubao> fsd = std::make_unique<FreeSpaceDoubao>();
// fsd 离开作用域时自动 delete,无论是否抛异常
// 不能拷贝(编译报错),只能移动 → 从根源杜绝 double-free
为什么 unique_ptr 零开销? 它在编译期内联了 delete 调用,生成的汇编代码与手写 delete 完全相同,没有任何运行时开销。ARM 平台上同样高效。
禁用拷贝构造的原理:= delete 告诉编译器"这个函数不存在",如果有人尝试拷贝,编译阶段就报错,而不是运行时崩溃。这是编译期安全优于运行时调试的典型实践。
2.2 预分配复用 —— 消除频繁分配/释放
核心思想:把"每帧申请-使用-释放"的模式,改为"启动时申请一次-每帧复用-程序结束释放"。
// 修复前:每帧 184KB 的分配/释放
void compute(...) {
cv::Mat1f columns(umax, vmax); // 每帧 new 184KB
// ... 使用 ...
} // 每帧 delete 184KB
// 修复后:成员变量预分配,每帧复用
class StixelWorld {
cv::Mat1f columns_; // 成员变量,生命周期 = 对象生命周期
};
void set_width_height_disparity(...) {
columns_.create(umax, vmax); // 只在尺寸变化时分配一次
}
void compute(...) {
cv::Mat1f& columns = columns_; // 引用复用,零分配
// ... 使用(直接覆盖写入,不需要先清空)...
}
为什么引用 cv::Mat1f& columns = columns_ 是安全的?
-
columns_是成员变量,在compute()调用期间不会被销毁 -
引用只是一个别名,不涉及拷贝或引用计数操作
-
对
columns的写入就是对columns_的写入,下一帧直接覆盖即可 -
添加尺寸检查
if (columns_.rows != umax || columns_.cols != vmax)是为了防御性编程——如果输入分辨率变化,自动重新分配
内存碎片消除的原理:分配器的压力从"每帧 N 次分配/释放"降低到"启动时 1 次分配"。free list 中不会产生大量空洞,mmap 区域也不会频繁伸缩,进程 RSS 稳定在一个固定值。
2.3 失败路径补 delete —— 确保所有路径都释放
核心思想:每一个 new 都必须在所有可能的退出路径上有对应的 ****delete。
// 修复前:只有成功路径释放
if (条件) {
vec.push_back(tmp); // 所有权转移给 vec
}
// 失败路径:tmp 泄漏
// 修复后:所有路径都释放
if (条件) {
vec.push_back(tmp); // 成功:所有权转移
} else {
delete tmp; // 失败:手动释放
}
更优的做法(未来改进方向):使用 std::unique_ptr 管理 tmp,成功时 release() 转移所有权,失败时自动释放:
auto tmp = std::make_unique<goyu_image_data>();
if (条件) {
vec.push_back(tmp.release()); // release() 放弃所有权,返回裸指针
}
// 失败时 tmp 自动 delete,不需要写 else
但本次修复保持了最小改动原则,只补了 else { delete tmp; },不改变数据结构的所有权语义。
2.4 单次 makeShared() —— 消除冗余分配
核心思想:同一份数据只创建一个共享实例,多个消费者共享所有权。
// 修复前:两次深拷贝
tree->setInputCloud(cloud.makeShared()); // PointCloud A
euclidean_cluster->setInputCloud(cloud.makeShared()); // PointCloud B(内容同A)
// 修复后:一次创建,共享引用
auto cloud_ptr = cloud.makeShared(); // PointCloud A,引用计数=1
tree->setInputCloud(cloud_ptr); // 引用计数=2
euclidean_cluster->setInputCloud(cloud_ptr); // 引用计数=3
// 函数结束:cloud_ptr 销毁 → 引用计数=2
// 下一帧 setInputCloud 替换旧的 → tree 和 euclidean_cluster 各自释放旧引用
// 当引用计数归零时,PointCloud A 被释放
shared_ptr** 引用计数的原理**:shared_ptr 内部有一个控制块(control block),存储引用计数(原子变量)和删除器。每次拷贝 shared_ptr,引用计数 +1(原子递增,线程安全);每次 shared_ptr 销毁或被重置,引用计数 -1;当计数归零时,调用删除器释放对象。
为什么这次修复既省内存又减碎片? 减少了一次 80KB 级的堆分配和一次对应的释放,降低了分配器的压力。
2.5 定期 swap 释放 capacity —— 回收异常大帧的内存
核心思想:在"性能"和"内存占用"之间取平衡——不每帧释放(避免重新分配开销),但定期检查并释放异常大的 capacity。
static int counter = 0;
if (++counter >= 100) { // 每 100 帧(约 10 秒)检查一次
counter = 0;
if (world_pixels.capacity() > 200) {
// swap 技巧:临时空 vector 与 vec 交换
// 临时 vector 析构时释放原内存(强制,不受 shrink_to_fit 非强制限制)
std::vector<std::vector<std::vector<float>>>().swap(world_pixels);
}
// ... 其他 vector 同理
}
为什么是 100 帧?
-
你的帧率约 10fps,100 帧 = 10 秒
-
每 10 秒做一次 capacity 检查和可能的 swap,性能开销可忽略(< 0.1ms)
-
如果某一帧场景异常复杂导致 capacity 暴涨,最多 10 秒后就会被回收
-
不会每帧 swap——那样会导致每帧重新分配,反而降低性能
为什么阈值是 200/2000/5000?
-
world_pixels和cluster_stixels:正常场景通常 < 100 个 stixel/cluster,阈值 200 意味着只有超过正常两倍时才释放 -
stixels_global:正常 < 1000 个点,阈值 2000 -
stixels:正常 < 2000 个,阈值 5000 -
output_arr:正常 < 100 个障碍物,阈值 500 -
阈值设置的原则:只释放异常大的 capacity,不干预正常范围的预分配(正常范围的预分配对性能有好处)
swap 技巧为什么比 shrink_to_fit() 可靠?
// shrink_to_fit:非强制,实现可能忽略
vec.shrink_to_fit(); // capacity 可能不变
// swap:强制释放
std::vector<T>().swap(vec); // capacity 一定变为 0
// 原理:临时空 vector 的 capacity=0,与 vec 交换后
// vec 的 capacity=0,临时 vector 持有原内存
// 临时 vector 离开作用域析构,原内存被释放
三、两个 Excel 监控文件的采集方法
这两个文件是你在车机板端采集的进程内存监控数据。以下是在 嵌入式 Linux 板端(ARM Cortex A55) 上生成这类 Excel 的完整步骤,你可以用同样的方法验证修复效果。
3.1 采集原理
Linux 内核通过 /proc 文件系统暴露每个进程的内存信息。最常用的是:
# 查看单个进程的内存状态
cat /proc/<PID>/status
# 关键字段:
# VmRSS: 实际物理内存(Resident Set Size),单位 KB —— 这是你最关心的
# VmSize: 虚拟内存大小
# VmPeak: 虚拟内存峰值
3.2 采集步骤
步骤 1:启动目标服务,获取 PID
# 启动 GODService(或你的启动脚本)
./GODService &
# 获取 PID
GOD_PID=$(pgrep -f GODService)
echo "GODService PID: $GOD_PID"
步骤 2:编写采集脚本
在板端创建 mem_monitor.sh:
#!/bin/bash
# mem_monitor.sh - 定时采集指定进程的内存并输出 CSV
PID=$1 # 目标进程 PID
INTERVAL=${2:-2} # 采样间隔(秒),默认 2 秒
OUTPUT=${3:-mem.csv}
# 写 CSV 表头
echo "时间戳,轮次,VmRSS(KB),VmSize(KB),VmPeak(KB)" > "$OUTPUT"
count=0
while true; do
# 检查进程是否还活着
if ! kill -0 "$PID" 2>/dev/null; then
echo "进程 $PID 已退出,采集结束"
break
fi
# 从 /proc/<PID>/status 提取内存字段
rss=$(grep VmRSS /proc/$PID/status | awk '{print $2}')
size=$(grep VmSize /proc/$PID/status | awk '{print $2}')
peak=$(grep VmPeak /proc/$PID/status | awk '{print $2}')
ts=$(date '+%Y-%m-%d %H:%M:%S')
count=$((count + 1))
echo "$ts,$count,$rss,$size,$peak" >> "$OUTPUT"
sleep "$INTERVAL"
done
步骤 3:运行采集
# 给脚本执行权限
chmod +x mem_monitor.sh
# 采集 GODService,每 2 秒一次,输出到 god_mem.csv
./mem_monitor.sh $GOD_PID 2 god_mem.csv
步骤 4:多进程同时采集(你的 Excel 中有多个服务)
如果要同时监控 PerceptionFusionService、TimSyncService、AppService、GODService 等多个服务,写一个批量采集脚本:
#!/bin/bash
# multi_mem_monitor.sh - 同时监控多个进程
INTERVAL=2
OUTPUT=multi_mem.csv
# 服务名列表
SERVICES=("PerceptionFusionService" "TimSyncService" "AppService" "GODService" "ParkingInservice" "LFusionService")
# 构建表头
header="时间戳,轮次"
for svc in "${SERVICES[@]}"; do
header="$header,${svc}_VmRSS(KB)"
done
echo "$header" > "$OUTPUT"
count=0
while true; do
count=$((count + 1))
ts=$(date '+%Y-%m-%d %H:%M:%S')
line="$ts,$count"
for svc in "${SERVICES[@]}"; do
pid=$(pgrep -f "$svc" | head -1)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
rss=$(grep VmRSS /proc/$pid/status | awk '{print $2}')
else
rss="N/A"
fi
line="$line,$rss"
done
echo "$line" >> "$OUTPUT"
sleep "$INTERVAL"
done
步骤 5:触发测试场景
- 空闲静置测试(对应《车子空闲静置内存监控情况》):
# 启动所有服务后,不做任何操作,挂机采集
./multi_mem_monitor.sh
# 等待 30 分钟(直到崩溃或手动停止)
- 操作泊车测试(对应《操作泊车内存监测统计1小时》):
# 启动所有服务后,持续进行泊车操作(前进/后退/打方向)
./multi_mem_monitor.sh
# 持续操作 1 小时
步骤 6:CSV 转 Excel
采集得到的是 CSV 文件,转为 Excel 有两种方式:
方式 A:板端用 Python(如果板端有 Python 和 openpyxl)
import pandas as pd
df = pd.read_csv('multi_mem.csv')
df.to_excel('内存监测统计.xlsx', index=False)
方式 B:把 CSV 拷到电脑上用 Excel 打开
# 板端 → 电脑(通过 adb / scp / U盘)
scp root@<车机IP>:/path/to/multi_mem.csv ./
# 电脑上用 Excel 打开 CSV,另存为 .xlsx
3.3 你的两个 Excel 文件的特征对照
从你上传的数据来看:
两个文件都包含了 6 个服务的 VmRSS 数据,说明是用类似上面的 multi_mem_monitor.sh 批量采集的。
3.4 修复验证建议
部署修复版本后,用完全相同的采集脚本和参数重新采集 1 小时操作泊车数据,对比:
修复前:GODService 99820 KB → 134904 KB(+35084 KB / 小时)
修复后:GODService 预计 < 104000 KB(+<5000 KB / 小时)
如果泄漏量下降 85% 以上,说明修复有效。剩余的少量增长通常是:
-
glibc 分配器的正常内存碎片(不可完全消除)
-
其他服务(如 PerceptionFusionService 泄漏 515M)对系统整体的影响
-
内核页缓存的增长(
/proc/meminfo中的Cached,可回收)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)