深入探究C++編程中的資源泄漏問(wèn)題以及排查方法
1、GDI對(duì)象泄漏
在Windows平臺(tái)上,做UI客戶端編程,很多時(shí)候都是使用系統(tǒng)GDI對(duì)象進(jìn)行窗口的繪制,常見(jiàn)的GDI對(duì)象有Pen(用來(lái)繪制線條的畫筆)、Brush(用來(lái)填充顏色的畫刷)、Bitmap(用來(lái)處理圖片的位圖)、Font(用來(lái)設(shè)置文字大小的字體)、Region(區(qū)域)、DC(設(shè)備上下文)等。
1.1、何為GDI資源泄漏?
對(duì)于Pen、Brush、Bitmap和Region等,在使用前我們需要調(diào)用創(chuàng)建這些對(duì)象的接口把對(duì)象創(chuàng)建出來(lái),比如CreatePen/CreatePenIndirect、CreateSolidBrush/CreateBrushIndirect、CreateFont/CreateFontIndirect、CreateCompatibleBitmap等API接口,然后在使用完這些對(duì)象后需要調(diào)用DeleteObject將對(duì)象釋放掉。對(duì)于DC對(duì)象,則一般調(diào)用GetDC去獲取窗口的DC對(duì)象,然后在不使用時(shí)需要調(diào)用ReleaseDC將DC釋放掉。如果不釋放這些對(duì)象,則會(huì)導(dǎo)致GDI對(duì)象泄漏。
在Windows程序中,一個(gè)進(jìn)程的GDI對(duì)象總數(shù)是有上限的,默認(rèn)情況下上限值為10000個(gè)。可以從如下的注冊(cè)表中可以看到,這個(gè)值是系統(tǒng)設(shè)置的默認(rèn)值,一般情況下不用修改,即使修改,也不能改成很大的值。
如果發(fā)生GDI對(duì)象泄漏的代碼段,頻繁地執(zhí)行,程序在持續(xù)運(yùn)行一段時(shí)間后,進(jìn)程的GDI對(duì)象總數(shù)接近或達(dá)到10000個(gè)上限。當(dāng)接近上限時(shí),就會(huì)出現(xiàn)GDI繪圖函數(shù)內(nèi)部發(fā)生錯(cuò)誤,返回失敗,導(dǎo)致窗口繪制異常。緊接著可能就會(huì)產(chǎn)生崩潰閃退。
1.2、使用GDIView工具排查GDI對(duì)象泄漏
GDI對(duì)象持續(xù)泄漏,對(duì)程序可能是致命的,一旦接近或達(dá)到上限,就會(huì)導(dǎo)致程序發(fā)聲崩潰閃退。GDI對(duì)象泄漏問(wèn)題,排查起來(lái)相對(duì)容易一些,先用GDIView工具先看一下是哪類GDI對(duì)象有泄漏:
然后有針對(duì)性的查看操作這類GDI對(duì)象的代碼,然后逐步縮小排查的范圍。
如果出現(xiàn)窗口繪制或顯示異常,或者程序無(wú)故閃退,可以到任務(wù)管理器中查看進(jìn)程的GDI對(duì)象總數(shù)的值:(默認(rèn)情況下不顯示GDI對(duì)象列,右鍵點(diǎn)擊標(biāo)題欄,在彈出窗口中勾選GDI對(duì)象選項(xiàng)即可顯示)
如果總數(shù)接近10000個(gè),肯定是GDI對(duì)象泄漏導(dǎo)致的??梢灾匦聠?dòng)程序,然后再任務(wù)管理器中持續(xù)觀察進(jìn)程的GDI對(duì)象總數(shù)。
1.3、有時(shí)可能需要結(jié)合其他方法去排查
有時(shí)也要結(jié)合其他方法來(lái)輔助定位,比如可以使用歷史版本比對(duì)法,看看是從哪天開(kāi)始出現(xiàn)泄漏。然后查看前一天svn或git上的代碼提交記錄,或者底層模塊庫(kù)發(fā)布記錄,這樣就能有效的縮小問(wèn)題的排查范圍。有次項(xiàng)目中出的問(wèn)題,就出在底層的WebRTC開(kāi)源庫(kù)中。當(dāng)時(shí)排查了UI層的代碼沒(méi)有找到泄漏點(diǎn),所以懷疑可能是底層模塊有問(wèn)題。
當(dāng)時(shí)找到了問(wèn)題的復(fù)現(xiàn)辦法,然后使用歷史版本比對(duì)法,確定了從哪一天開(kāi)始出現(xiàn)泄漏。然后查看了svn上的代碼提交記錄以及底層庫(kù)的發(fā)布記錄, 發(fā)現(xiàn)出問(wèn)題前一天底層開(kāi)源組件組發(fā)布了新版本的WebRTC開(kāi)源庫(kù),在這個(gè)版本中開(kāi)源組件組為了處理一個(gè)bug,添加了一段代碼,于是找開(kāi)源組件的同事排查一下他們提交的代碼,看看是否存在GDI泄漏。一小段時(shí)間后,他們給出結(jié)論,說(shuō)他們新加的代碼沒(méi)問(wèn)題,應(yīng)該是其他模塊引發(fā)的。但根據(jù)歷史版本比對(duì)法的對(duì)比,問(wèn)題應(yīng)該就出在WebRTC開(kāi)源庫(kù)中,但開(kāi)源組件組始終覺(jué)得他們的代碼沒(méi)問(wèn)題。
于是我到開(kāi)源組件組那邊查看svn上他們的代碼修改記錄,看到他們新增的一段代碼果然有問(wèn)題,如下所示:
#if defined (WEBRTC_WIN) //修正程序開(kāi)啟DWM導(dǎo)致的鼠標(biāo)位置問(wèn)題 int desktop_horzers = GetDeviceCaps( GetDC(nullptr) DESKTOPHORZRES); // 問(wèn)題就出在這個(gè)GetDC上 int horzers = GetDeviceCaps(GetDC(nullptr),HORZRES); float scale_rate=(float)desktop_horzers/(float)horzers; relative_position.set( relative_ position.x()*scale_rate, relative_ position.y()*scale_rate ); #endif
這段代碼中,他們調(diào)用GetDC接口獲取窗口的DC對(duì)象,在使用完DC對(duì)象后,沒(méi)有調(diào)用ReleaseDC將DC對(duì)象釋放掉,所以導(dǎo)致了DC對(duì)象的泄漏。修改后的代碼如下:
#if defined (WEBRTC_WIN)? ? //修正程序開(kāi)啟DWM導(dǎo)致的鼠標(biāo)位置問(wèn)題? ? HDC hDC = ::GetDC(nullptr);? ? int desktop_horzers = GetDeviceCaps( hDC, DESKTOPHORZRES);? ? int horzers = GetDeviceCaps(hDC,HORZRES);? ? float scale_rate=(float)desktop_horzers/(float)horzers;? ? relative_position.set( relative_ position.x()*scale_rate,?? ? ? ? relative_ position.y()*scale_rate );? ? ::ReleaseDC(nullptr, hDC);#endif
至于開(kāi)源組件的同事沒(méi)找到問(wèn)題,可能是他們對(duì)UI編程不熟悉導(dǎo)致的。
1.4、如何保證沒(méi)有GDI對(duì)象泄漏?
要保證不出現(xiàn)GDI對(duì)象泄漏,在GDI對(duì)象使用完成后要將之刪除或釋放掉,如果不刪除或釋放,則會(huì)導(dǎo)致GDI泄漏。比如使用CreateXXXXXX創(chuàng)建的GDI對(duì)象,使用完后,要用DeleteObject釋放;調(diào)用LoadXXXXXX函數(shù)去加載圖片資源,使用完后,也要用DeleteObject釋放;調(diào)用CreateXXXDC創(chuàng)建的DC對(duì)象,使用完后,要用DeleteDC去釋放;調(diào)用GetDC獲取到的DC對(duì)象,使用完后,要用ReleaseDC釋放。
調(diào)用不用的接口去創(chuàng)建或獲取GDI對(duì)象,釋放時(shí)也要調(diào)用對(duì)應(yīng)的釋放接口,不能混淆!在這里給大家大概的羅列一下:
創(chuàng)建或獲取GDI對(duì)象 | 刪除或釋放GDI對(duì)象 |
CreatePen/CreatePenIndirect(pen畫筆對(duì)象)、CreateSolidBrush/CreateBrushIndirect(brush畫刷對(duì)象)、CreateFont/CreateFontIndirect(Font字體對(duì)象)、CreateCompatibleBitmap(BItmap位圖對(duì)象) | 對(duì)于Create出來(lái)的對(duì)象,要調(diào)用DeleteObject釋放 |
CreateDC/CreateCompatibleDC(創(chuàng)建DC對(duì)象) | 調(diào)用DeleteDC釋放 |
GetDC(獲取DC對(duì)象) | 調(diào)用ReleaseDC釋放 |
LoadBitmap(加載Bitmap位圖) | 調(diào)用DeleteObject釋放 |
LoadImage(加載圖片資源) | 如果加載的是Bitmap位圖,則調(diào)用DeleteObject釋放; 如果加載的是Cursor光標(biāo),則調(diào)用DestroyCursor釋放; 如果加載的是Icon圖標(biāo),則調(diào)用DestroyIcon釋放。 |
對(duì)于上面提到的創(chuàng)建GDI對(duì)象的API函數(shù),在釋放時(shí)該調(diào)用哪個(gè)接口,直接到MSDN上查看API接口的Remarks部分就會(huì)找到對(duì)應(yīng)的說(shuō)明。比如創(chuàng)建兼容位圖的API函數(shù)CreateCompatibleBItmap,在Remaks部分的說(shuō)明如下:
再比如加載圖片的API函數(shù)LoadImage,其在Remarks部分的說(shuō)明如下:
在調(diào)用Windows系統(tǒng)API函數(shù)遇到問(wèn)題時(shí),需要到微軟MSDN幫助頁(yè)面中查看API函數(shù)的詳細(xì)說(shuō)明(可能會(huì)給出調(diào)用函數(shù)時(shí)的注意事項(xiàng),或者調(diào)用函數(shù)的示例代碼等),在說(shuō)明中可能會(huì)找到相關(guān)的原因!會(huì)使用MSDN,是一個(gè)Windows開(kāi)發(fā)人員最基本的要求!
2、進(jìn)程句柄泄漏
進(jìn)程句柄包括文件句柄(打開(kāi)文件時(shí)產(chǎn)生的句柄)、注冊(cè)表句柄(打開(kāi)注冊(cè)表節(jié)點(diǎn)時(shí)產(chǎn)生的句柄)、事件句柄、信號(hào)量句柄、線程句柄(創(chuàng)建線程時(shí)產(chǎn)生的句柄)、進(jìn)程句柄(創(chuàng)建子進(jìn)程時(shí)產(chǎn)生的句柄)等。
2.1、何為進(jìn)程句柄泄漏?
這些句柄在使用完成后需要及時(shí)釋放,如果不釋放,則會(huì)造成句柄泄漏。一般調(diào)用CloseHandle去釋放句柄,比如進(jìn)程句柄、線程句柄、事件句柄、文件句柄等。當(dāng)然也有部分句柄需要對(duì)應(yīng)的接口去釋放,比如注冊(cè)表句柄需要調(diào)用RegCloseKey去關(guān)閉。
在Winows系統(tǒng)中,進(jìn)程句柄數(shù)也是有上限的,默認(rèn)也是10000個(gè),也有對(duì)應(yīng)的注冊(cè)表項(xiàng)。當(dāng)進(jìn)程的句柄數(shù)接近或達(dá)到10000個(gè)上限時(shí),就會(huì)導(dǎo)致后續(xù)產(chǎn)生句柄的操作會(huì)執(zhí)行失敗,比如調(diào)用CreateThread去創(chuàng)建新的線程會(huì)失敗。關(guān)于進(jìn)程GDI對(duì)象上限值的注冊(cè)表設(shè)置路徑為:
計(jì)算機(jī)\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
不僅進(jìn)程的GDI對(duì)象有上限,進(jìn)程的句柄數(shù)(比如線程句柄、事件句柄等句柄)也是有上限的,默認(rèn)也是10000個(gè)。注冊(cè)表對(duì)應(yīng)的節(jié)點(diǎn)配置如下:
這兩個(gè)配置項(xiàng)的說(shuō)明如下:
1)GDIProcessHandleQuota項(xiàng):設(shè)置GDI句柄數(shù)量,默認(rèn)值為2710(16進(jìn)制)/10000(10進(jìn)制),該值的允許范圍為 256 ~ 16384 ,將其調(diào)整為大于默認(rèn)的10000的值。如果您的系統(tǒng)配置了2G或更多內(nèi)容,不妨將其設(shè)置為允許的最大值 16384(10進(jìn)制)。
2)USERProcessHandleQuota項(xiàng):設(shè)置用戶句柄數(shù)量,默認(rèn)值同樣為2710(16進(jìn)制)/10000(10進(jìn)制),該值的允許范圍為 200 ~ 18000 ,將其調(diào)整為更多的數(shù)值。同樣地,對(duì)于具有2GB或更多物理內(nèi)存的系統(tǒng),不妨將用戶句柄數(shù)直接設(shè)置為上限 18000(10進(jìn)制)。
2.2、創(chuàng)建線程時(shí)的線程句柄泄漏
以前我們?cè)陧?xiàng)目中就遇到這樣的問(wèn)題,有的業(yè)務(wù)子系統(tǒng)是通過(guò)https和平臺(tái)服務(wù)器交互的,客戶端每執(zhí)行一個(gè)https操作時(shí)都會(huì)創(chuàng)建一個(gè)線程去執(zhí)行,但創(chuàng)建線程后沒(méi)有調(diào)用CloseHanlde將線程句柄關(guān)閉掉,導(dǎo)致線程句柄發(fā)生泄漏,當(dāng)多次執(zhí)行https操作導(dǎo)致線程句柄過(guò)多,導(dǎo)致后續(xù)再去創(chuàng)建線程創(chuàng)建失敗了。可以到Process Explorer中查看進(jìn)程都占用哪些具體的句柄:
這個(gè)地方需要注意一下,調(diào)用CloaseHandle將線程句柄釋放掉:
HANDLE hThread = ::CreateThread( NULL, NULL, ProcessProc, this, NULL, NULL ); if ( hThread != NULL ) { CloseHandle( hThread ); }
調(diào)用CloseHandle將句柄釋放掉,并不表示將線程結(jié)束掉,線程是否結(jié)束是要看線程函數(shù)的,線程函數(shù)退出了,則線程就結(jié)束了。
線程結(jié)束了,不會(huì)自動(dòng)關(guān)閉線程句柄。對(duì)于線程函數(shù),還有一個(gè)細(xì)節(jié),發(fā)起線程創(chuàng)建的CreateThread函數(shù)返回了,不代表線程的代碼已經(jīng)執(zhí)行到線程函數(shù)中了。這點(diǎn)我們?cè)陧?xiàng)目中遇到過(guò)這類的場(chǎng)景。當(dāng)時(shí)的問(wèn)題場(chǎng)景是,線程函數(shù)中訪問(wèn)了一個(gè)指針變量,將該指針變量的值初始化為NULL的操作放在CreateThread函數(shù)調(diào)用之后,如下所示:
// 1、指針變量定義 CVideoDec* g_pVideoDec; // 2、創(chuàng)建線程 HANDLE hThread = ::CreateThread( NULL, NULL, ProcessProc, this, NULL, NULL ); if ( hThread != NULL ) { CloseHandle( hThread ); } g_pVideoDec = NULL; // 對(duì)指針變量進(jìn)行初始化 // 3、線程函數(shù),函數(shù)中訪問(wèn)了指針變量g_pVideoDec DWORD WINAPI ProcessCachedMsgThreadProc( LPVOID lpParameter ) { // 線程函數(shù)中訪問(wèn)到了該指針變量 g_pVideoDec->StartDec(); return 1; }
當(dāng)然這種做法是不規(guī)范的,后來(lái)發(fā)現(xiàn)程序會(huì)時(shí)不時(shí)崩潰在線程函數(shù)中,使用Windbg分析下來(lái)得知是線程中訪問(wèn)了未初始化的變量,但這個(gè)問(wèn)題不是必現(xiàn)的。這個(gè)不必現(xiàn),就和CreateThread函數(shù)返回后線程是否執(zhí)行到線程函數(shù)中有關(guān)。
有時(shí),CreateThread返回時(shí)還沒(méi)執(zhí)行到線程函數(shù)中,緊接著就去初始化指針變量的值,是不會(huì)崩潰的。但如果CreateThread返回時(shí)已經(jīng)執(zhí)行到線程函數(shù)中,就會(huì)訪問(wèn)未初始化的指針變量,Release下未初始化的內(nèi)存是個(gè)隨機(jī)值,即指針變量的值為隨機(jī)值,所以一般都會(huì)引發(fā)異常。
3、內(nèi)存泄漏
內(nèi)存泄漏是C++程序使用動(dòng)態(tài)申請(qǐng)的內(nèi)存時(shí)容易出現(xiàn)的一類典型內(nèi)存問(wèn)題。動(dòng)態(tài)申請(qǐng)內(nèi)存的方式有多種,比如使用new(要用delete去釋放),比如使用malloc(要用free去釋放),再比如調(diào)用系統(tǒng)API函數(shù)HeapCreate或者HeapAlloc(要用HeapFree去釋放),還有可以調(diào)用API函數(shù)VirtualAlloc(要用VirtualFree去釋放),當(dāng)然還有其他的API函數(shù)。動(dòng)態(tài)申請(qǐng)的內(nèi)存沒(méi)有釋放,則會(huì)導(dǎo)致內(nèi)存泄漏。
之所以會(huì)導(dǎo)致內(nèi)存泄漏,可能是忘記釋放,也可能是寫了釋放內(nèi)存的代碼,但因?yàn)榉N種原因沒(méi)有執(zhí)行到內(nèi)存釋放的代碼,后面這類情況有一定的隱蔽性。下面我們重點(diǎn)說(shuō)一下后面的這類情況。
3.1、在多態(tài)中沒(méi)有將父類的析構(gòu)函數(shù)聲明為virtual函數(shù),導(dǎo)致沒(méi)有執(zhí)行到子類的析構(gòu)函數(shù)
比如如下的多態(tài)代碼:
class CBase { public: CBase(); ~CBase(); // 沒(méi)有將父類的析構(gòu)函數(shù)設(shè)置為虛函數(shù) } class CDerived : public class CBase { public: CDerived(); ~CDerived(); } // 將new出來(lái)的子類對(duì)象賦值給父類指針,就是多態(tài) CBase* pBase = new CDerived; // ... // 中間代碼省略 delete pBase;
上述代碼,因?yàn)?strong>沒(méi)有將父類的析構(gòu)函數(shù)~CBase設(shè)置為虛函數(shù),導(dǎo)致執(zhí)行到delete pBase;時(shí)沒(méi)有調(diào)用子類的析構(gòu)函數(shù),導(dǎo)致子類的部分內(nèi)存沒(méi)有釋放,從而引發(fā)內(nèi)存泄漏。特別是新人比較容易犯這類錯(cuò)誤,之前在幫新人排查問(wèn)題時(shí)遇到過(guò),這個(gè)場(chǎng)景下的內(nèi)存泄漏具有一定的隱蔽性。
如果不是析構(gòu)函數(shù),是其他的成員函數(shù),如果父類的接口沒(méi)有聲明為virtual,多態(tài)就不會(huì)生效,會(huì)導(dǎo)致子類重寫的成員函數(shù)不會(huì)被執(zhí)行到,子類重寫的成員函數(shù)中可能包含了重要的業(yè)務(wù)代碼,這樣就會(huì)導(dǎo)致重要的業(yè)務(wù)代碼沒(méi)有執(zhí)行到,導(dǎo)致業(yè)務(wù)出現(xiàn)異常。
之前我們這邊的新人將代碼移植到國(guó)產(chǎn)化機(jī)器上時(shí)就遇到過(guò),新人忘記在父類的接口前添加virtual聲明,導(dǎo)致子類的接扣執(zhí)行不到,導(dǎo)致業(yè)務(wù)出現(xiàn)異常,當(dāng)時(shí)他查了很久沒(méi)找出問(wèn)題,后來(lái)找我去排查,找到這個(gè)原因,所以對(duì)這個(gè)問(wèn)題印象很深!
所以,我們?cè)诙xC++類時(shí),如果該類可能會(huì)被繼承,一般都要將父類的析構(gòu)函數(shù)設(shè)置為虛函數(shù),防止出現(xiàn)上述多態(tài)場(chǎng)景下子類的析構(gòu)函數(shù)執(zhí)行不到導(dǎo)致內(nèi)存泄漏的問(wèn)題。當(dāng)然,設(shè)置虛函數(shù)有一定的副作用,如果一個(gè)類中包含虛函數(shù),則類中會(huì)自動(dòng)添加一個(gè)虛函數(shù)表指針,此外虛函數(shù)調(diào)用時(shí)也涉及到二次尋址問(wèn)題(效率上略有影響)。
3.2、使用智能指針shared_ptr發(fā)生循環(huán)引用問(wèn)題,導(dǎo)致內(nèi)存泄漏
使用shared_ptr可能會(huì)出現(xiàn)循環(huán)引用問(wèn)題,這使用shared_ptr智能指針的一個(gè)典型問(wèn)題(也是一個(gè)關(guān)于shared_ptr智能指針的面試題),場(chǎng)景是兩個(gè)類中都包含了指向?qū)Ψ降膕hared_ptr對(duì)象,這樣會(huì)導(dǎo)致new出來(lái)的兩個(gè)類沒(méi)有走析構(gòu),引發(fā)內(nèi)存泄漏問(wèn)題。
循環(huán)引用問(wèn)題的示意圖如下:
相關(guān)代碼如下:
#include <iostream> #include<memory> using namespace std; class B; class A{ public: shared_ptr<B> bptr; ~A(){cout<<"~A()"<<endl;} } class B { public: shared_ptr<A> aptr; ~B( ){cout<<"~B()"<<endl;} } int main() { shared_ptr<A> pa(new A()); // 引用加1 shared_ptr<B> pb(new B()); // 引用加1 pa->bptr = pb; // 引用加1 pa->aptr = pa; // 引用加1 return 0; }
執(zhí)行到上述return 0這句代碼時(shí),指向A和B兩個(gè)對(duì)象的引用計(jì)數(shù)都是2。當(dāng)退出main函數(shù)時(shí),先析構(gòu)shared_ptr<B> pb對(duì)象,B對(duì)象的引用計(jì)數(shù)減1,B對(duì)象的引用計(jì)數(shù)還為1,所以不會(huì)delete B對(duì)象,不會(huì)進(jìn)入B對(duì)象析構(gòu)函數(shù),所以B類中的shared_ptr<A> aptr成員不會(huì)析構(gòu),所以此時(shí)A對(duì)象的引用計(jì)數(shù)還是2。當(dāng)析構(gòu)shared_ptr<A> pa時(shí),A的引用計(jì)數(shù)減1,A對(duì)象的引用計(jì)數(shù)變?yōu)?,所以不會(huì)析構(gòu)A對(duì)象。所以上述代碼會(huì)導(dǎo)致A和B兩個(gè)new出的對(duì)象都沒(méi)釋放,導(dǎo)致內(nèi)存泄漏。
為了解決上述問(wèn)題,引入了weak_ptr,可以將類中包含的shared_ptr成員換成weak_ptr,如下:
相關(guān)代碼如下:
#include <iostream> #include<nemory> using namespace std; class B; class A{ public: weak_ptr<B> bptr; // 使用weak_ptr替代shared_ptr ~A(){cout<<"~A()"<<endl;} } class B { public: weak_ptr<A> aptr; // 使用weak_ptr替代shared_ptr ~B( ){cout<<"~B()"<<endl;} } int main() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->bptr = pb; pa->aptr = pa; return 0; }
3.3、第三方注入庫(kù)有內(nèi)存泄漏,導(dǎo)致進(jìn)程有內(nèi)存泄漏
第三庫(kù)注入到我們程序進(jìn)程中有兩個(gè)典型的場(chǎng)景,一種是輸入法模塊的注入,一種是第三方安全軟件的注入。輸入法要支持所有進(jìn)程的文字輸入,正式通過(guò)遠(yuǎn)程注入到所有進(jìn)程的模塊去感知用戶的輸入的。第三方安全軟件,為了監(jiān)控軟件的數(shù)據(jù)操作,一般也是需要遠(yuǎn)程注入到進(jìn)程中的。
之前項(xiàng)目中就遇到過(guò)第三方安全軟件的注入模塊有內(nèi)存泄漏,導(dǎo)致進(jìn)程內(nèi)存耗盡,引發(fā)程序閃退。對(duì)于這類問(wèn)題,可能其他軟件運(yùn)行不會(huì)觸發(fā)內(nèi)存泄漏,只有我們的軟件才會(huì)觸發(fā)內(nèi)存泄漏,這個(gè)需要拿出足夠的證據(jù)證明是第三方安全軟件的注入模塊引起的內(nèi)存泄漏,否則客戶會(huì)認(rèn)為這是我們軟件的問(wèn)題,因?yàn)槠渌浖紱](méi)問(wèn)題,客戶可能會(huì)不承認(rèn)這與第三方安全軟件有關(guān)。當(dāng)時(shí)的問(wèn)題原因是,第三方安全軟件處理UDP數(shù)據(jù)監(jiān)控的代碼有內(nèi)存泄漏,因?yàn)槲覀兊能浖写罅康囊粢曨l數(shù)據(jù)收發(fā),走的是UDP,所以觸發(fā)了第三方安全軟件注入模塊的內(nèi)存泄漏。在給出足夠的證據(jù)后,客戶找到第三方安全軟件提供商,然后安全廠商才修復(fù)了這個(gè)bug。
3.4、內(nèi)存泄漏的危害
如果發(fā)生內(nèi)存泄漏的代碼,不會(huì)頻繁地的執(zhí)行,只是偶爾的執(zhí)行一下,不會(huì)引起太大的問(wèn)題。但如果有內(nèi)存泄漏的代碼,被頻繁地執(zhí)行,則會(huì)頻繁地泄漏(內(nèi)存不釋放),最終可能會(huì)導(dǎo)致進(jìn)程的內(nèi)存耗盡,引發(fā)Out of memory(內(nèi)存耗盡)的崩潰。
進(jìn)程啟動(dòng)時(shí),系統(tǒng)會(huì)給進(jìn)程分配指定大小的虛擬內(nèi)存。以32位程序?yàn)槔到y(tǒng)會(huì)分配4GB的虛擬內(nèi)存,其中用戶態(tài)虛擬內(nèi)存2GB,內(nèi)核態(tài)虛擬內(nèi)存2GB,一般內(nèi)存泄漏的代碼都在用戶態(tài),所以內(nèi)存持續(xù)泄漏會(huì)導(dǎo)致用戶態(tài)虛擬內(nèi)存被用盡,引發(fā)Out of memory的崩潰。當(dāng)然,對(duì)于64位程序,會(huì)分配足夠大的虛擬內(nèi)存。但用戶的電腦可能很多天不關(guān)機(jī),軟件一直在持續(xù)的運(yùn)行,如果有持續(xù)的內(nèi)存泄漏,總有內(nèi)存用盡的那一天。
我們可以通過(guò)Windows自帶的任務(wù)管理器:
去持續(xù)觀察目標(biāo)進(jìn)程的內(nèi)存變化情況,如果內(nèi)存持續(xù)增長(zhǎng)不回落,則可能存在內(nèi)存泄漏。
此外,Windows自帶的任務(wù)管理器看不到進(jìn)程的總的虛擬內(nèi)存占用,可以使用Process Explorer工具查看進(jìn)程占用的總虛擬內(nèi)存,該工具顯示的是用戶態(tài)的虛擬內(nèi)存占用:
我們一般只需要關(guān)注用戶態(tài)的虛擬內(nèi)存,因?yàn)闃I(yè)務(wù)代碼占用的是用戶態(tài)的虛擬內(nèi)存。
我們的程序是32位的,系統(tǒng)給進(jìn)程分配了4GB的虛擬內(nèi)存,其中用戶態(tài)虛擬內(nèi)存占2GB,內(nèi)核態(tài)虛擬內(nèi)存占2GB,從上圖中看,當(dāng)前程序進(jìn)程的用戶態(tài)虛擬內(nèi)存占用達(dá)到1.7GB,已經(jīng)快接近2GB的上限了,可能再運(yùn)行一會(huì),2GB用戶態(tài)的內(nèi)存就要耗盡了,程序就會(huì)閃退!
注意,Process Explorer工具默認(rèn)是不顯示Virtual Size虛擬內(nèi)存列,需要右鍵點(diǎn)擊進(jìn)程列表的標(biāo)題欄,點(diǎn)擊“Select Columns”,在彈出的窗口中點(diǎn)擊“Process Memory”標(biāo)簽頁(yè),然后將“Virtual Size”選項(xiàng):
3.5、內(nèi)存泄漏的排查
內(nèi)存泄漏問(wèn)題的排查,相對(duì)比較麻煩,但可以使用一些工具去分析。
3.5.1、Windows平臺(tái)上內(nèi)存泄漏的排查
在Windows平臺(tái)上,可以使用Windbg(使用!heap命令)、umdh.exe(該工具位于Windbg的安裝目錄中)、DebugDiag、VMMAP以及Visual C++專用的Visual Leak Detector等工具。對(duì)于Visual Leak Detector工具,需要將相關(guān)的庫(kù)編譯到模塊中。其他幾個(gè)工具,則可以直接使用。
此外,從Visual Studio 2019的16.9版本開(kāi)始,Visual Studio引入了google的強(qiáng)大內(nèi)存監(jiān)測(cè)工具AddressSanitizer(AddressSanitizer原先只在Linux系統(tǒng)中被支持,繼承在gcc中),就像gcc那樣提供編譯選項(xiàng)上的支持:
在安裝高版本的Visual Studio時(shí),可以將“C++ AddressSanitizer”安裝選項(xiàng)勾選上,這樣Visual Studio中就支持AddressSanitizer了。
AddressSanitizer(簡(jiǎn)稱ASan)是google提供的一款面向C/C++語(yǔ)言的內(nèi)存錯(cuò)誤問(wèn)題檢查工具,它可以檢測(cè)出堆溢出(Heap buffer overflow)、棧溢出(Stack buffer overflow)、全局變量越界(Global buffer overflow)、已釋放內(nèi)存使用(Use after free )、初始化順序(Initialization order bugs)、內(nèi)存泄漏(Use after free )等多個(gè)內(nèi)存問(wèn)題。
AddressSanitizer項(xiàng)目地址:https://github.com/google/sanitizers/wiki/AddressSanitizer
參考文檔頁(yè)面:AddressSanitizerAlgorithm · google/sanitizers Wiki · GitHub
如果要使用AddressSanitizer內(nèi)存檢測(cè)工具,必須要使用Visual Studio 2019的16.9及以上的版本。此外,AddressSanitizer不能像Windbg那樣獨(dú)立運(yùn)行,直接附加到目標(biāo)進(jìn)程上去分析,需要使用AddressSanitizer相關(guān)編譯選項(xiàng)重新編譯代碼才行。
3.5.2、Linux平臺(tái)上內(nèi)存泄漏的排查
在Linux平臺(tái)上,常用的內(nèi)存檢測(cè)工具有Valgrind和AddressSanitizer,這兩個(gè)工具各有優(yōu)勢(shì)。
Valgrind工具可以直接監(jiān)測(cè)目標(biāo)進(jìn)程,不需要重新編譯代碼,用起來(lái)比較方便。但Valgrind在監(jiān)測(cè)內(nèi)存時(shí)比較消耗內(nèi)存,同時(shí)會(huì)嚴(yán)重拖慢程序的運(yùn)行速度,這對(duì)于需要實(shí)時(shí)響應(yīng)的服務(wù)器來(lái)講,是個(gè)很大的問(wèn)題。
AddressSanitizer是google出品的內(nèi)存檢測(cè)工具,gcc4.8及以上版本才內(nèi)置了AddressSanitizer,通過(guò)編譯選項(xiàng)去使用該工具(需要重新編譯代碼),該工具會(huì)占用更少的內(nèi)存,不會(huì)明顯拖慢程序的運(yùn)行速度。不過(guò)要使用該工具,需要將gcc4.8及以上的版本才行。
4、最后
上面詳細(xì)講解了GDI對(duì)象泄漏、進(jìn)程句柄資源泄漏和內(nèi)存泄漏三大類問(wèn)題,希望能給大家提供一定的借鑒和參考。
以上就是深入探究C++編程中的資源泄漏問(wèn)題以及排查方法的詳細(xì)內(nèi)容,更多關(guān)于C++編程資源泄漏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Pipes實(shí)現(xiàn)LeetCode(195.第十行)
這篇文章主要介紹了Pipes實(shí)現(xiàn)LeetCode(195.第十行),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08C語(yǔ)言實(shí)現(xiàn)解析csv格式文件的示例代碼
CSV,有時(shí)也稱為字符分隔值,其文件以純文本形式存儲(chǔ)表格數(shù)據(jù)(數(shù)字和文本),本文為大家整理了C語(yǔ)言解析csv文件的方法,需要的可以參考一下2023-06-06opencv 做人臉識(shí)別 opencv 人臉匹配分析
opencv 人臉識(shí)別通過(guò)級(jí)聯(lián)分類器對(duì)特征的分級(jí)篩選來(lái)確定是否是人臉,每個(gè)節(jié)點(diǎn)的正確識(shí)別率很高,但正確拒絕率很低,任一節(jié)點(diǎn)判斷沒(méi)有人臉特征則結(jié)束運(yùn)算,宣布不是人臉2012-11-11C++?sqlite3數(shù)據(jù)庫(kù)配置使用教程
SQLite 是一種嵌入式的關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),它是一個(gè)開(kāi)源項(xiàng)目,已經(jīng)被廣泛應(yīng)用于各種應(yīng)用程序和操作系統(tǒng)中,這篇文章主要介紹了C++?sqlite3數(shù)據(jù)庫(kù)配置使用,需要的朋友可以參考下2023-08-08C語(yǔ)言中static與sizeof查缺補(bǔ)漏篇
static在修飾變量的時(shí)候,如果是修飾全局變量,則跟全局變量功能一樣;如果是修改局部變量,則每次調(diào)用的時(shí)候,保持著上一次的值;而sizeof是用來(lái)判斷一個(gè)變量及數(shù)據(jù)類型所占字節(jié)數(shù)的,下面我們?cè)敿?xì)來(lái)看看2022-07-07C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05