尧图精选

内存泄漏问题

🕒 发布时间:2026/9/1 19:31:21 📁 来源:尧图网络
内存泄漏问题内存泄漏的工具sudo apt-get installvalgrindvalgrind --leak-checkyes ./GODService /home/Downloads/0826pm/tt7内存泄漏原理、修复原理与数据采集方法一、内存泄漏的详细原理1.1 什么是内存泄漏内存泄漏的本质是程序向操作系统申请了内存堆分配但在不再需要时没有归还导致这块内存既无法被程序复用也无法被操作系统回收。随着运行时间累积进程占用的物理内存RSS持续增长最终触发 OOMOut-Of-Memory被系统杀死。在嵌入式 LinuxARM Cortex A55通常 512MB~2GB 内存上这个问题尤为致命——你的车机挂机半小时就崩溃正是因为多个服务同时泄漏系统内存耗尽。1.2 五个泄漏点的底层原理泄漏点 1裸指针无拷贝控制 → double-free / 泄漏原理C 中用new分配的对象必须用delete释放。如果类持有裸指针成员但没有正确实现拷贝构造函数和拷贝赋值运算符即违反Rule of Three/Five会出现两种灾难场景 Adouble-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_ptrStixelWorld stixelworld_暂时不会发生拷贝但这是一颗定时炸弹——任何后续维护者如果不小心按值传递或赋值就会触发 double-free。更关键的是裸指针在异常发生时如构造函数中第二个new抛出异常第一个已分配的对象无法被自动释放。泄漏点 2每帧创建大临时对象 → 内存碎片原理这不是严格意义上的泄漏内存最终会被释放但效果与泄漏相同——进程 RSS 持续增长且不回落。这是嵌入式平台上最常见、也最容易被忽视的问题。时间线每帧 100ms即每秒 10 帧 t0ms: cv::Mat1f columns(128, 360) → 申请 184,320 字节184KB t50ms: columns 使用完毕函数返回 → 释放 184KB t100ms: 下一帧再次申请 184KB → 但上一块可能还没被归还给 OS t150ms: 再次释放... ...为什么会碎片glibc 的malloc使用ptmalloc分配器它的工作机制是小块内存 128KB通过brk()从堆顶分配释放后不会立即归还操作系统而是留在进程的 free list 中供下次复用但如果多次申请/释放的大小不一致或者中间有其他分配插桩free list 中会留下大量无法合并的空洞你的columns是 184KB128KB走mmap分配路径——mmap的内存释放后理论上会归还 OS但频繁mmap/munmap会导致虚拟地址空间碎片和缺页中断开销更严重的是你的compute()函数中同时还有std::vectorfloat roadDisp(vmax)— 每帧创建std::vectorint lowerPathDb(columns.rows)— 每帧创建std::vectorint 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后续会被使用 } // ❌ 失败路径isGoodfalse 或 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)下调用但如果回放目录中有损坏的图片文件isGoodfalse每遇到一张坏图就泄漏数 MB回放几千张图后泄漏量非常可观。泄漏点 4重复 makeShared() → 双倍内存 碎片原理PCL 的PointCloud::makeShared()会在堆上创建一个新的PointCloud对象并返回shared_ptr。// 原代码 tree-setInputCloud(cloud.makeShared()); // 创建 PointCloud A引用计数1 euclidean_cluster-setInputCloud(cloud.makeShared()); // 创建 PointCloud B引用计数1cloud是栈上的pcl::PointCloudpcl::PointXYZ对象第一次makeShared()把cloud的数据深拷贝到堆上的 PointCloud AA 的引用计数为 1被tree持有第二次makeShared()又把cloud的数据深拷贝到堆上的 PointCloud BB 的引用计数为 1被euclidean_cluster持有函数结束时A 和 B 分别被 tree 和 euclidean_cluster 释放因为它们是成员变量持有的是上一帧的点云setInputCloud 会替换旧的问题在于两个点云内容完全相同却分配了两份内存。如果点云有 5000 个点每个点 16 字节 80KB每帧就多浪费 80KB。同时两次独立的堆分配增加了内存碎片的概率。泄漏点 5vector capacity 不释放 → 内存持续占用原理std::vector的clear()方法只将 size 置为 0不会释放 capacity已分配的内存。某一帧场景特别复杂路口、多车多人 stixels 从 0 增长到 3000 个 → vector 自动扩容capacity 变为 4096 占用内存4096 × sizeof(Stixel) ≈ 4096 × 24 98KB 后续帧场景简单直道、无障碍物 stixels.clear() → size0但 capacity 仍为 4096 这 98KB 内存被持续占用不会归还操作系统 如果 world_pixels三维 vector某一帧有 2000 个 stixel 外层 vector capacity2048 每个内层 vectorstd::vectorfloat 也有自己的 capacity 最内层 vectorfloat3个元素每个都有独立的堆分配 allocator overhead 总占用可能达到 2048 × (24 24 12) ≈ 123KB 大量堆碎片std::vector::shrink_to_fit()理论上可以释放多余 capacity但 C 标准规定这是一个非强制请求“non-binding request”实现可以忽略。因此最可靠的方式是swap 技巧std::vectorT().swap(vec); // 创建临时空 vector与 vec 交换临时 vector 析构时释放原内存你的代码中world_pixels和cluster_stixels是public成员在detect()中被clear()在get_visual()中被重新填充。如果某一帧get_visual()没有被调用visual_common_resultfalse它们就保持空但 capacity 很大的状态持续占用内存。二、修复的原理2.1 RAII 与 unique_ptr —— 用对象生命周期管理内存核心思想资源获取即初始化RAII——将资源内存的生命周期绑定到对象的生命周期上。对象构造时获取资源对象析构时自动释放资源无论正常返回还是异常抛出。// 修复前手动管理容易出错 FreeSpaceDoubao* fsd new FreeSpaceDoubao(); // 获取 // ... 中间可能抛异常 ... delete fsd; // 释放如果抛异常就到不了这里 // 修复后unique_ptr 自动管理 std::unique_ptrFreeSpaceDoubao fsd std::make_uniqueFreeSpaceDoubao(); // 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_uniquegoyu_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::vectorstd::vectorstd::vectorfloat().swap(world_pixels); } // ... 其他 vector 同理 }为什么是 100 帧你的帧率约 10fps100 帧 10 秒每 10 秒做一次 capacity 检查和可能的 swap性能开销可忽略 0.1ms如果某一帧场景异常复杂导致 capacity 暴涨最多 10 秒后就会被回收不会每帧 swap——那样会导致每帧重新分配反而降低性能为什么阈值是 200/2000/5000world_pixels和cluster_stixels正常场景通常 100 个 stixel/cluster阈值 200 意味着只有超过正常两倍时才释放stixels_global正常 1000 个点阈值 2000stixels正常 2000 个阈值 5000output_arr正常 100 个障碍物阈值 500阈值设置的原则只释放异常大的 capacity不干预正常范围的预分配正常范围的预分配对性能有好处swap 技巧为什么比shrink_to_fit()可靠// shrink_to_fit非强制实现可能忽略 vec.shrink_to_fit(); // capacity 可能不变 // swap强制释放 std::vectorT().swap(vec); // capacity 一定变为 0 // 原理临时空 vector 的 capacity0与 vec 交换后 // vec 的 capacity0临时 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 count0 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 - 同时监控多个进程 INTERVAL2 OUTPUTmulti_mem.csv # 服务名列表 SERVICES(PerceptionFusionService TimSyncService AppService GODService ParkingInservice LFusionService) # 构建表头 header时间戳,轮次 for svc in ${SERVICES[]}; do header$header,${svc}_VmRSS(KB) done echo $header $OUTPUT count0 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 rssN/A fi line$line,$rss done echo $line $OUTPUT sleep $INTERVAL done步骤 5触发测试场景空闲静置测试对应《车子空闲静置内存监控情况》# 启动所有服务后不做任何操作挂机采集 ./multi_mem_monitor.sh # 等待 30 分钟直到崩溃或手动停止操作泊车测试对应《操作泊车内存监测统计1小时》# 启动所有服务后持续进行泊车操作前进/后退/打方向 ./multi_mem_monitor.sh # 持续操作 1 小时步骤 6CSV 转 Excel采集得到的是 CSV 文件转为 Excel 有两种方式方式 A板端用 Python如果板端有 Python 和 openpyxlimport pandas as pd df pd.read_csv(multi_mem.csv) df.to_excel(内存监测统计.xlsx, indexFalse)方式 B把 CSV 拷到电脑上用 Excel 打开# 板端 → 电脑通过 adb / scp / U盘 scp root车机IP:/path/to/multi_mem.csv ./ # 电脑上用 Excel 打开 CSV另存为 .xlsx3.3 你的两个 Excel 文件的特征对照从你上传的数据来看两个文件都包含了 6 个服务的 VmRSS 数据说明是用类似上面的multi_mem_monitor.sh批量采集的。3.4 修复验证建议部署修复版本后用完全相同的采集脚本和参数重新采集 1 小时操作泊车数据对比修复前GODService 99820 KB → 134904 KB35084 KB / 小时 修复后GODService 预计 104000 KB5000 KB / 小时如果泄漏量下降 85% 以上说明修复有效。剩余的少量增长通常是glibc 分配器的正常内存碎片不可完全消除其他服务如 PerceptionFusionService 泄漏 515M对系统整体的影响内核页缓存的增长/proc/meminfo中的Cached可回收
上一篇/下一篇内容由系统自动关联 返回资讯列表 →