转自:http://chaishushan.blog.163.com/blog/static/130192897200911685559809/

内存泄漏时程序开发中经常遇到的问题. 而且出现内存泄漏很难检测,
但是其导致的结果却是灾难性的. 这里讲一下opencv中内存泄漏检测
的一些技巧.

OpenCV中关于内存管理主要涉及到以下3个函数:

代码: 全选
CV_IMPL void  cvSetMemoryManager( CvAllocFunc alloc_func, CvFreeFunc free_func, void* userdata );
CV_IMPL void* cvAlloc( size_t size );
CV_IMPL void  cvFree_( void* ptr );

还有一个对应cvFree_的宏:

代码: 全选
#define cvFree(ptr) (cvFree_(*(ptr)), *(ptr)=0)

宏cvFree的用处是在释放ptr指针对应的内存后, 将ptr设置为NULL.

这里我们先做个假设: opencv中所有的内存分配和释放都是通过cvAlloc和cvFree合作完成的.
如果你使用cvAlloc分配一个内存, 然后用delete来是释放内存是错误的(切记)!!!

因此, 如果我们能够跟踪到cvAlloc/cvFree的调用流程, 就可以分析内存泄漏的情况了.

一般情况下, 一个cvAlloc分配的内存最终必然要对应cvFree来释放, 如果cvAlloc/cvFree不是
匹配出现, 那么可以认为出现了内存泄漏.

为此, 我们需要定义自己的内存管理函数, 然后通过cvSetMemoryManager装载到opencv中.
内存管理函数的类型如下:

代码: 全选
typedef void* (CV_CDECL *CvAllocFunc)(size_t size, void* userdata);
typedef int (CV_CDECL *CvFreeFunc)(void* pptr, void* userdata);

其中的userdata是用户通过cvSetMemoryManager来设置的. 我们可以简单的吧userdata当作一个
容器指针, 在每次执行我们自己的alloc_func/free_func函数时, 将内存的分配/释放情况记录到
userdata对应的容器.

为此, 我自己简单设计了一个MemTracker类:

代码: 全选
#ifndef OPENCV_MEM_TRACKER_H
#define OPENCV_MEM_TRACKER_H

#include <stdio.h>
#include <vector>

// 内存泄漏追踪

class MemTracker
{
public:
   MemTracker(void);
   ~MemTracker(void);

private:

// 登记分配/释放的内存

void regAlloc(void *ptr, size_t size);
   void regFree(void *ptr);

// 输出泄漏的内存

int output(FILE* fp=stderr);

private:
   
   // 分配内存

static void* alloc_func(size_t size, void *userdata);

// 释放内存

static int free_func(void *ptr, void *userdata);

private:

struct Ptr
   {
      void *ptr;      // 内存地址
      size_t size;   // 内存大小

Ptr(void *ptr, size_t size)
      {
         this->ptr = ptr;
         this->size = size;
      }
   };

// 记录当前使用中的内存

std::vector<Ptr>   m_memTracker;
};

#endif   // OPENCV_MEM_TRACKER_H

类的实现如下:

代码: 全选
#include "MemTracker.h"

#include <assert.h>
#include <cv.h>

MemTracker::MemTracker(void)
{
   // 注册管理函数

cvSetMemoryManager(alloc_func, free_func, (void*)this);
}

MemTracker::~MemTracker(void)
{
   // 取消管理函数

cvSetMemoryManager(NULL, NULL, NULL);

// 输出结果

this->output();
}

// 登记分配/释放的内存

void MemTracker::regAlloc(void *ptr, size_t size)
{
   m_memTracker.push_back(Ptr(ptr, size));
}

void MemTracker::regFree(void *ptr)
{
   int i;
   for(i = 0; i < m_memTracker.size(); ++i)
   {
      // 删除记录

if(m_memTracker[i].ptr == ptr)
      {
         m_memTracker[i] = m_memTracker[m_memTracker.size()-1];
         m_memTracker.pop_back();
         return;
      }
   }
}

// 输出泄漏的内存

int MemTracker::output(FILE* fp)
{
   int n = m_memTracker.size();
   int i;

for(i = 0; i < n; ++i)
   {
      fprintf(fp, "%d: %p, %u/n", i, m_memTracker[i].ptr, m_memTracker[i].size);
   }

return n;
}

// 分配内存

void* MemTracker::alloc_func(size_t size, void *userdata)
{
   assert(size > 0 && userdata != NULL);

// 分配内存

void *ptr = malloc(size);
   if(!ptr) return NULL;

// 登记

MemTracker *tracker = (MemTracker*)userdata;
   tracker->regAlloc(ptr, size);

//

return ptr;
}

// 释放内存

int MemTracker::free_func(void *ptr, void *userdata)
{
   assert(ptr != NULL && userdata != NULL);

// 释放内存

free(ptr);

// 登记

MemTracker *tracker = (MemTracker*)userdata;
   tracker->regFree(ptr);

// CV_OK == 0

return 0;
}

MemTracker在构造的时候会注册自己的内存管理函数, 在析构的时候会输出没有被释放的内存.
下面我们编写一个测试程序:

代码: 全选
#include <cv.h>
#include <highgui.h>

#include "MemTracker.h"

int main()
{
   MemTracker mem;

IplImage *img = cvLoadImage("lena.jpg", 1);
   if(!img) return -1;

// 没有释放img内存

// cvReleaseImage(&img);

return 0;
}

在main函数退出的时候mem会被析构, 然后输出内存的泄漏情况. 下面是在我的电脑上测试的结果:

代码: 全选
C:/work/vs2005/MemTracker/debug>MemTracker.exe
0: 00C750C0, 112
1: 00D90040, 786432

OK, 先说到这里吧, 下次再补充...

前面我们已经解决了内存泄漏的检测, 但是在出现内存泄漏的时候我们怎么才能
跟踪到出现内存泄漏的代码呢? 如果能够调试到没有被释放内存对应的cvAlloc函数就好了.

这个我们可以通过m_memTracker[i].ptr来比较内存的地址来检测, 例如在alloc_func中
添加以下代码, 然后设置断点:

代码: 全选
// 检测00C750C0内存
if(ptr == (void*)00C750C0)
{
    // 设置断点
}

但是这个方法可能还有缺陷. 因为每次运行程序的时候, 内存的布局可能是有区别的.
最好的方法是把cvAlloc的调用顺序记录下来.

变动的部分代码:

代码: 全选
class MemTracker
{
   struct Ptr
   {
      void *ptr;      // 内存地址
      size_t size;   // 内存大小
      int   id;

Ptr(void *ptr, size_t size, int id)
      {
         this->ptr = ptr;
         this->size = size;
         this->id = id;
      }
   };

// 记录当前使用中的内存

std::vector<Ptr>   m_memTracker;

// alloc_func对应的编号

int               m_id;
};
MemTracker::MemTracker(void)
{
   m_id = 0;

// 注册管理函数

cvSetMemoryManager(alloc_func, free_func, (void*)this);
}
void MemTracker::regAlloc(void *ptr, size_t size)
{
   // 每次记录一个新的m_id
   m_memTracker.push_back(Ptr(ptr, size, m_id++));
}
// 输出泄漏的内存

int MemTracker::output(FILE* fp)
{
   int n = m_memTracker.size();
   int i;

for(i = 0; i < n; ++i)
   {
      fprintf(fp, "%d: %p, %u/n", m_memTracker[i].id, m_memTracker[i].ptr, m_memTracker[i].size);
   }

return n;
}

以后就可以根据m_memTracker[i].id来设置断点跟踪调试. 因为每次运行程序的时候, cvAlloc的调用次序是不变
的, 因此可以认为每次cvAlloc对应的id也是不变的. 这样就可以根据id来追踪出现内存泄漏的cvAlloc了.

对于"OpenCV扩展库", 可以将MemTracker直接集成到CvxApplication中, 这样就可以默认进行内存泄漏检测了.
内存检测先说到这里, 下一节我会简要分析一下OpenCV的cvAlloc等源代码 

前面的帖子中我们已经讨论了cvAlloc/cvFree_/cvSetMemoryManager等函数的使用技巧.
下面开始分析OpenCV中以上函数的实现代码. 我觉得如果在阅读代码之前, 如果能对函数的
用法有个基本的认识, 那么对于分析源代码是很有帮助的.

代码: 全选
CV_IMPL  void*  cvAlloc( size_t size )
{
    void* ptr = 0;
    
    CV_FUNCNAME( "cvAlloc" );

__BEGIN__;

if( (size_t)size > CV_MAX_ALLOC_SIZE )
        CV_ERROR( CV_StsOutOfRange,
                  "Negative or too large argument of cvAlloc function" );

ptr = p_cvAlloc( size, p_cvAllocUserData );
    if( !ptr )
        CV_ERROR( CV_StsNoMem, "Out of memory" );

__END__;

return ptr;
}

从代码我们可以直观的看出, cvAlloc分配的内存不得大于CV_MAX_ALLOC_SIZE, 即使是使用我们
自己的内存管理函数也会有这个限制.

然后通过p_cvAlloc对应的函数指针对应的函数来分配内存. p_cvAlloc是一个全局static变量, 对应的
还有p_cvFree和p_cvAllocUserData, 分别对应释放内存函数和用户数据. 它们的定义如下:

代码: 全选
// pointers to allocation functions, initially set to default
static CvAllocFunc p_cvAlloc = icvDefaultAlloc;
static CvFreeFunc p_cvFree = icvDefaultFree;
static void* p_cvAllocUserData = 0;

默认的内存管理函数分别为icvDefaultAlloc和icvDefaultFree(icv开头的表示为内部函数), 用户数据指针为空.

继续跟踪默认的内存分配函数icvDefaultAlloc, 代码如下:

代码: 全选
static void*
icvDefaultAlloc( size_t size, void* )
{
    char *ptr, *ptr0 = (char*)malloc(
        (size_t)(size + CV_MALLOC_ALIGN*((size >= 4096) + 1) + sizeof(char*)));

if( !ptr0 )
        return 0;

// align the pointer
    ptr = (char*)cvAlignPtr(ptr0 + sizeof(char*) + 1, CV_MALLOC_ALIGN);
    *(char**)(ptr - sizeof(char*)) = ptr0;

return ptr;
}

内部使用的是C语言中的malloc函数, 在分配的时候多申请了CV_MALLOC_ALIGN*((size >= 4096) + 1) + sizeof(char*)
大小的空间. 多申请空间的用处暂时先不分析.

下面的cvAlignPtr函数用于将指针对其到CV_MALLOC_ALIGN边界, 对于我们常规的PC来说是32bit, 也就是4字节.
cvAlignPtr函数在后面会详细讨论.

下面语句将ptr0记录到(ptr - sizeof(char*)), 可以把它看作一个指针. 最后返回ptr.
细心的朋友可能会发现, 前面malloc分配的是ptr0, 现在返回的却是ptr, 这个是为什么呢?

这个的原因还是先放下(我也不懂), 但是返回ptr而不返回ptr0带来的影响至少有2个:

1. 返回的ptr指针不能通过C语言的free函数释放(这也是cvAlloc/cvFree必须配对使用的原因).
2. 在cvFree的时候, 可以根据(ptr - sizeof(char*))对应的值来检测该内存是不是由icvDefaultAlloc申请.

这样应该说可以增加程序的健壮性, icvDefaultFree可以不傻瓜似的对于任何指针都进行释放.

下面来看看cvAlignPtr函数:

代码: 全选
CV_INLINE void* cvAlignPtr( const void* ptr, int align=32 )
{
    assert( (align & (align-1)) == 0 );
    return (void*)( ((size_t)ptr + align - 1) & ~(size_t)(align-1) );
}

该函数的目的主要是将指针ptr调整到align的整数倍

其中align必须为2的幂, assert语言用于该检测. 语句(align & (align-1))
一般用于将align的最低的为1的bit位设置为0. 如果为2的幂那么就只有1个为1
的bit位, 因此语句(x&(x-1) == 0)可以完成该检测.

return语句简化后为 (ptr+align-1)&~(align-1), 等价于((ptr+align-1)/align)*align.
就是找到不小于ptr, 且为align整数倍的最小整数, 这里对应为将指针对其到4字节(32bit).

cvFree_函数和cvAlloc类似, 就不详细分析了:

代码: 全选
CV_IMPL  void  cvFree_( void* ptr )
{
    CV_FUNCNAME( "cvFree_" );

__BEGIN__;

if( ptr )
    {
        CVStatus status = p_cvFree( ptr, p_cvAllocUserData );
        if( status < 0 )
            CV_ERROR( status, "Deallocation error" );
    }

__END__;
}

p_cvFree默认值为icvDefaultFree:

代码: 全选
static int
icvDefaultFree( void* ptr, void* )
{
    // Pointer must be aligned by CV_MALLOC_ALIGN
    if( ((size_t)ptr & (CV_MALLOC_ALIGN-1)) != 0 )
        return CV_BADARG_ERR;
    free( *((char**)ptr - 1) );

return CV_OK;
}

最后我们简要看下cvSetMemoryManager函数, 它主要用来设置用户自己定义的内存管理函数:

代码: 全选
CV_IMPL void cvSetMemoryManager( CvAllocFunc alloc_func, CvFreeFunc free_func, void* userdata )
{
    CV_FUNCNAME( "cvSetMemoryManager" );

__BEGIN__;

// 必须配套出现
    
    if( (alloc_func == 0) ^ (free_func == 0) )
        CV_ERROR( CV_StsNullPtr, "Either both pointers should be NULL or none of them");

p_cvAlloc = alloc_func ? alloc_func : icvDefaultAlloc;
    p_cvFree = free_func ? free_func : icvDefaultFree;
    p_cvAllocUserData = userdata;

__END__;
}

如果函数指针不为空, 则记录到p_cvAlloc和p_cvFree指针, 如果为空则恢复到默认的内存管理函数.
需要注意的是if语句的条件(alloc_func == 0) ^ (free_func == 0), 只有当2个函数1个为NULL, 1个
不为NULL的时候才会出现, 出现这个的原因是内存管理函数的分配和释放函数不匹配了, 这个是不允许的.

因此, 我们需要设置自己的内存管理函数, 就需要同时指定alloc_func和free_func函数, 清空的时候
则把2个参数都设置NULL就可以了.

今天再来补充一个小技巧 

我们前面通过cvSetMemoryManager函数来重新设置了自己的内存管理函数.
但是前面也说到过, 如果cvAlloc/cvFree覆盖的周期和MemTracker相交, 那么
内存会出现错误.

即,

1. 原来OpenCV默认函数分配的内存可能使用我们自己的cvFree函数来释放.
2. 我们自己定义的cvAlloc分配的内存可能使用原来OpenCV默认的函数来释放.

这都会造成错误!

其实我们定义的目的只是要统计内存的使用情况, 我们并不想真的使用自己的函数的管理
OpenCV的内存. 道理很简单, OpenCV的内存经过优化, 对齐到某个字节, 效率更好.

如果能获取OpenCV原始的内存管理函数就好了, 但是没有这样的函数!!!

但是, 我们任然有方法来绕过这个缺陷.

我们可以在MemTracker::alloc_func函数进入之后, 在用cvSetMemoryManager恢复原来的
内存管理函数, 这样我们统计目的也达到了, 而且还是用了OpenCV本身的函数来分配内存.

代码如下:

代码: 全选
void* MemTracker::alloc_func(size_t size, void *userdata)
{
   assert(size > 0 && userdata != NULL);
   
   // 取消管理函数

cvSetMemoryManager(NULL, NULL, NULL);
   
   // 用OpenCV的方式分配内存

void *ptr = cvAlloc(size);

// 登记

if(ptr)
   {
      MemTracker *tracker = (MemTracker*)userdata;
      tracker->regAlloc(ptr, size);
   }

// 重新注册注册管理函数

cvSetMemoryManager(alloc_func, free_func, userdata);

return ptr;
}

MemTracker::free_func的方法和上面类似, 就不贴代码了.

以后我们就可以透明的使用MemTracker了, 不管MemTracker对象在那个地方定义,
它对OpenCV的内存管理都不会有影响.

前面的方法虽然使得CvxMemTracker可以在任何地方使用, 但是可能带来理解的难度.

因为 在cvAlloc之后进入的是 MemTracker::alloc_func, 但是在这个函数中又调用了cvAlloc!
这看起来很像一个无穷递归调用!!

但是实际的运行结果却没有出现无穷递归导致的栈溢出情形. 仔细分析就知道原理了:

1. 定义MemTracker对象

中间调用了 cvSetMemoryManager(alloc_func, free_func, (void*)this); 函数,
设置 MemTracker::alloc_func 为分配函数.

2. 调用cvAlloc

内部执行到 MemTracker::alloc_func, 依次执行

代码: 全选
// 取消管理函数
   
   cvSetMemoryManager(NULL, NULL, NULL);

此刻, 分配函数又恢复为OpenCV的icvDefaultAlloc函数.
执行

代码: 全选
// 用OpenCV的方式分配内存
   
   void *ptr = cvAlloc(size);
   
   // 登记
   
   if(ptr)
   {
      CvxMemTracker *tracker = (CvxMemTracker*)userdata;
      tracker->regAlloc(ptr, size);
   }

这里的cvAlloc函数内部调用的是icvDefaultAlloc函数, 并不是MemTracker::alloc_func !!
就是这里了, alloc_func内部虽然调用了cvAlloc, 但是没有执行到alloc_func.

因此alloc_func不会出现递归.

最新的代码可以参考下面:
http://opencv-extension-library.googlec ... mTracker.h
http://opencv-extension-library.googlec ... racker.cpp

OpenCV中的内存泄漏检测相关推荐

  1. 【安装配置】安装适用于 Linux 的 Windows 子系统 WSL ,完成 Clion 中对内存泄漏检测工具 Valgrind 的配置,亲测可用

    关键词:[Linux] [WSL] [Clion] [Valfrind] 一.前言 今天在回答一个粉丝的评论(关于C++ delete 和 delete[ ])时,引出上面的系列问题,具体流程如下: ...

  2. 内存泄漏检测工具:Deleaker 2022

    用于 C++.C#..NET 和 DELPHI 的分析器 与 Visual Studio 完全集成 • 发现任何泄漏:内存.GDI.句柄和其他 • 配置非托管和 .NET 代码 • 支持 32 位和 ...

  3. 监视和检测Java应用程序中的内存泄漏

    因此,您的应用程序内存不足,您日夜不停地分析应用程序,以期捕获对象中的内存漏洞. 后续步骤将说明如何监视和检测您的内存泄漏,以确保您的应用程序安全. 1.怀疑内存泄漏 如果您怀疑有内存泄漏,可以使用一 ...

  4. VC++6.0中内存泄漏检测 转

    最近看了周星星 Blog 中的一篇文章:"VC++6.0中内存泄漏检测",受益匪浅,便运行其例子代码想看看 Output 窗口中的输出结果,可惜怎么弄其输出都不是预期的东西,郁闷了 ...

  5. 在vs中使用 vld 进行内存泄漏检测

    下载 vld https://github.com/KindDragon/vld 安装 vld 后记录安装目录 C:\Program Files (x86)\Visual Leak Detector ...

  6. VS环境中进行内存泄漏的检测

    转自:VS环境中进行内存泄漏的检测 - 星辰风 - 博客园 根据MSDN中的介绍,亲测整理. 本篇比较长,如不愿花费太多时间,可只看第一段和第四段,甚至只看第四段. 内存泄漏,即未能正确释放以前分配的 ...

  7. Unix下C程序内存泄漏检测工具Valgrind安装与使用

    Valgrind是一款用于内存调试.内存泄漏检测以及性能分析的软件开发工具. Valgrind的最初作者是Julian Seward,他于2006年由于在开发Valgrind上的工作获得了第二届Goo ...

  8. JVM内存泄漏检测与处理

    JVM内存泄漏检测与处理(JVM Memory Leak detection and handling) JVM垃圾回收机制的原则和方法 JVM垃圾回收中一个基本原则是对象没有被引用或则引用其它对象, ...

  9. android内存泄漏原因分析,Android Studio3.6的内存泄漏检测功能 VS LeakCanary

    2020年2月,谷歌发布了Android Studio 3.6版.它包括一个新的"内存泄漏检测"功能.这是否意味着我们不再需要流行的内存泄漏检测库"Leak Canary ...

最新文章

  1. 参加完Python培训后有哪些就业方向
  2. 使用struts2制作后台中的问题记录
  3. The Way to TiDB 3.0 and Beyond (下篇)
  4. VS 中配置使用Visual SVN系列 一:SVN Server下载和安装
  5. vb的GUID生成算法
  6. SQL创建表格——手写代码
  7. 使用TFS CI/CD 完成 VSTS 插件自动化部署和发布
  8. C++学习之普通函数指针与成员函数指针
  9. 系统目录 linux命令,linux基础命令之系统目录(示例代码)
  10. 读《编码-藏匿在计算机软硬件背后的语言》有感
  11. UEdit初始化加载内容偶尔失败,解决
  12. 工具使用-curl/wget
  13. 一份比较详尽的ubuntu下替代windows下的软件列表
  14. vulhub靶场sql注入漏洞复现
  15. Android逆向:某鹰直播Lua脚本解密
  16. 光纤交换机配置zone
  17. 深度神经网络如何训练,深度神经网络怎么用
  18. C# 实现对接电信交费易自动缴费
  19. DQN:Playing Atari with Deep Reinfocement Learning
  20. Kafka序列化器,分区器,拦截器,消息累加器

热门文章

  1. WifiP2pService工作流程
  2. POJ - 3186 Treats for the Cows DP
  3. python建站与java建站有何不同_详解模板建站和定制建站的不同之处
  4. 每日起床前做这五个动作,可保障你终身不感冒——奥运福娃设计师韩美林老师亲测可用
  5. 【实现js和css互通、共享常量参数值】js如何获取CSS/SCSS/LESS的常量、CSS/SCSS/LESS又是如何获取js的值(或者说js是如何主动推送参数给CSS使用的)?
  6. 【亲测可用】改变鼠标样式
  7. 在Ubuntu 16.04.3 TLS上玩转tls协议的简单demo
  8. org.hibernate.hibernate.connection.release_mode
  9. IOS入门-TargetAction
  10. Web.Config文件配置之限制上传文件大小和时间