OpenCV中的内存泄漏检测
生活随笔
收集整理的這篇文章主要介紹了
OpenCV中的内存泄漏检测
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
轉自: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中的内存泄漏检测的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OpenCV中的内存泄露问题(cvLoa
- 下一篇: OpenCV 2.2.0 CvvImag