記一次記憶體洩露除錯
首先介紹一下相關背景。最近在測試一個程式時發現,在任務執行完成之後,從工作管理員上來看,記憶體並沒有下降到理論值上。程式在啟動完成之後會佔用一定的記憶體,在執行任務的時候,會動態建立一些記憶體,用於儲存任務的執行狀態,比如掃描了哪些頁面,在掃描過程中一些收發包的記錄等等資訊。這些中間資訊在任務結束之後會被清理掉。任務結束之後,程式只會儲存執行過的任務列表,從理論上講,任務結束之後,程式此時所佔記憶體應該與程式剛啟動時佔用記憶體接近,但是實際觀察的結果就是任務結束之後,與剛啟動之時記憶體佔用差距在100M以上,這很明顯不正常,當時我的第一反應是有記憶體洩露
記憶體洩露排查
既然有記憶體洩露,那麼下一步就是開始排查,由於程式是採用MFC編寫的,那麼自然就得找MFC的記憶體洩露排查手段。
根據網上找到的資料,MFC在DEBUG模式中可以很方便的整合記憶體洩露檢查機制的。
首先在 stdafx.h
#define _CRTDBG_MAP_ALLO
#include <crtdbg.h>
再在程式退出的地方加入程式碼
_CrtDumpMemoryLeaks();
如果發生記憶體洩露的話,在除錯執行結束之後,觀察VS的輸出情況可以看到如下內容
Detected memory leaks! Dumping objects -> .\MatriXayTest.cpp(38) : {1301} normal block at 0x0000000005584D30, 40 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete.
在輸出中會提示有記憶體洩露,下面則是洩露的具體內容,MatriXayTest.cpp 是發生洩露的程式碼檔案,括號中的38代表程式碼所在行數,大括號中1301代表這是程式的第1301次分配記憶體,或者說第1301次執行malloc操作,再往後就是記憶體洩露的地址,以及洩露的大小,這個地址是程序啟動之後隨機分配的地址,參考意義不大。下面一行表示,當前記憶體中的具體值,從值上來看應該是分配了記憶體但是沒有初始化。
根據這個線索,我們來排查,找到第38行所在位置
int *p = new int[10];
_CrtDumpMemoryLeaks();
return nRetCode;
記憶體洩露正是出現在new了10個int型別的資料,但是後面沒有進行delete操作,正是這個操作導致了記憶體洩露。
到此為止,檢測工具也找到了,下面就是加上這段程式碼,執行發生洩露的程式,檢視結果
再漫長的等待任務執行完成並自動停止之後,我發現居然沒有發現記憶體洩露!!!
我又重複執行任務多次,發現結果是一樣的,這個時候我開始懷疑是不是這個庫不準,於是我在資料節點的類中新增建構函式,統計任務執行過程中建立了多少個節點,再在析構中統計析構了多少個節點,最終發現這兩個資料完全相同。也就是說真的沒有發生記憶體洩露。
在這裡我也疑惑了,難道是工作管理員有問題?帶著這個疑問,我自己寫了一段程式碼,在程式中不定時列印記憶體佔用情況,結果發現雖然與工作管理員有差異,但是結果是類似的,釋放之後記憶體並沒有大幅度的下降。
我帶著疑問查詢資料的過程的漫長過程中,發現工作管理員的顯示記憶體佔用居然降下去了,我統計了一下時間,應該是在任務結束之後的30分鐘到40分鐘之間。帶著這個疑問,我又重新發起任務,在任務結束,並等待一定時間之後,記憶體佔用果然降下去了。
這裡我得出結論 程式中執行delete操作之後,系統並不會真的立即回收操作,而是保留這個記憶體一定時間,以便後續該程序在需要分配時直接使用
結論驗證
科學一般來說需要大膽假設,小心求證,既然上面根據現象做了一些猜想,下面就需要對這個猜想進行驗證。
首先來驗證作業系統在程式呼叫delete之後不會真的執行delete操作。我使用下面的程式碼進行驗證
//定義一個佔1M記憶體的結構
struct data{
char c[1024 * 1024];
}
data* pa[1024] = {0};
for (int i = 0; i < 1024; i++)
{
pa[i] = new data;
//這裡執行一下資料清零操作,以便作業系統真正為程式分配記憶體
//有時候呼叫new或者malloc操作,作業系統只是保留對應地址,但是並未真正分配實體記憶體
//操作會等到程序真正開始操作這塊記憶體或者程序需要分配的記憶體總量達到一個標準時才真正進行分配
memset(pa[i], 0x00, sizeof(data));
}
printf("記憶體分配完畢,按任意鍵開始釋放記憶體...\n");
getchar();
for (int i = 0; i < 1024; i++)
{
delete pa[i];
}
printf("記憶體釋放完畢,按任意鍵退出\n");
_CrtDumpMemoryLeaks();
char c = getchar();
通過除錯這段程式碼,在剛開始執行,沒有執行到new操作的時候,程序佔用記憶體在2M左右,執行到第一個迴圈結束,分配記憶體後,佔用記憶體大概為1G,在執行完delete之後,記憶體並沒有立馬下降到初始的2M,而是穩定在150M左右,過一段時間之後,程式所佔用記憶體會將到2M左右。
接著對上面的程式碼做進一步修改,來測試記憶體使用時間長度與回收所需時間的長短的關係。這裡仍然使用上面定義的結構體來做嘗試
data* pa[1024] = {0};
for (int i = 0; i < 1024; i++)
{
pa[i] = new data;
memset(pa[i], 0x00, sizeof(data));
}
printf("記憶體分配完畢,按任意鍵開始寫資料到記憶體\n");
getchar();
//寫入隨機字串
srand((unsigned) time(NULL));
DWORD dwStart = GetTickCount();
DWORD dwEnd = dwStart;
printf("開始往目標記憶體中寫入資料\n");
while ((dwEnd - dwStart) < 1 * 60 * 1000) //執行時間為1分鐘
{
for (int i = 0; i < 1024; i++)
{
for (int j = 0; j < 1024; j++)
{
int flag = rand() % 3;
switch (flag)
{
case 1:
{
//生成大寫字母
pa[i]->c[j] = (char)(rand() % 26) + 'A';
}
break;
case 2:
{
//生成小寫字母
pa[i]->c[j] = (char)(rand() % 26) + 'a';
}
break;
case 3:
{
//生成數字
pa[i]->c[j] = (char)(rand() % 10) + '0';
}
break;
default:
break;
}
}
}
dwEnd = GetTickCount();
}
printf("資料寫入完畢,按任意鍵開始釋放記憶體...\n");
getchar();
for (int i = 0; i < 1024; i++)
{
delete pa[i];
}
printf("記憶體釋放完畢,按任意鍵退出\n");
_CrtDumpMemoryLeaks();
char c = getchar();
後面就不放測試的結果了,我直接說結論,同一塊記憶體使用時間越長,作業系統真正保留它的時間也會越長。短時間內差別可能不太明顯,長時間執行,這個差別可以達到秒級甚至分。
我記得當初上作業系統這門課程的時候,老師說過一句話:一個在過去使用時間越長的資源,在未來的時間內會再次使用到的概率也會越高,基於這一原理,操作會保留這塊記憶體一段時間,如果程式在後面再次申請對應結構時,作業系統會直接將之前釋放的記憶體拿來使用。為了驗證這一現象,我來一個小的測試
int *p1 = new int[1024];
memset(p, 0x00, sizeof(int) * 1024);
delete[] p;
int* p2= new int[1024];
通過除錯發現兩次返回的地址相同,也就驗證了之前說的內容
總結
最後來總結一下結論,有時候遇到delete記憶體後任務管理器或者其他工具顯示記憶體佔用未減少或者減少的量不對時,不一定是發生了記憶體洩露,也可能是作業系統的記憶體管理策略:程式呼叫delete後作業系統並沒有真的立即回收對應記憶體,它只是暫時做一個標記,後續真的需要使用相應大小的記憶體時會直接將對應記憶體拿出來以供使用。而具體什麼時候真正釋放,應該是由作業系統進行巨集觀調控。
我覺得這次暴露出來的問題還是自己基礎知識掌握不紮實,如果當時我能早點回想起來當初上課時所講的內容,可能也就不會有這次針對一個錯誤結論,花費這麼大的精力來測試。當然這個世界上沒有如果,我希望看到這篇博文的朋友,能少跟風學習新框架或者新語言,少被營銷號帶節奏,沉下心了,補充計算機基礎知識,必將受益匪淺。