亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入探究C++編程中的資源泄漏問(wèn)題以及排查方法

 更新時(shí)間:2023年10月08日 09:20:35   作者:dvlinker  
在C++程序開(kāi)發(fā)維護(hù)過(guò)程中,時(shí)常會(huì)遇到資源泄漏問(wèn)題,比如GDI對(duì)象泄漏、進(jìn)程線程句柄泄漏以及內(nèi)存泄漏問(wèn)題,今天我們就來(lái)深入探討一下這幾類資源泄漏以及排查這些泄露的辦法,需要的朋友可以參考下

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)文章

  • C 語(yǔ)言指針概念的詳解

    C 語(yǔ)言指針概念的詳解

    這里主要介紹C 語(yǔ)言指針,這里整理了詳細(xì)的資料,對(duì)指針做了詳細(xì)說(shuō)明及簡(jiǎn)單示例代碼幫助大家理解什么是指針,有興趣的小伙伴可以參考下
    2016-08-08
  • Pipes實(shí)現(xiàn)LeetCode(195.第十行)

    Pipes實(shí)現(xiàn)LeetCode(195.第十行)

    這篇文章主要介紹了Pipes實(shí)現(xiàn)LeetCode(195.第十行),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • C++中的for-each循環(huán)使用

    C++中的for-each循環(huán)使用

    范圍循環(huán)是C++11引入的特性,用于簡(jiǎn)化數(shù)組和容器的遍歷過(guò)程,它通過(guò)直接操作元素而不是使用索引或迭代器,范圍循環(huán)可以使用引用或const修飾符來(lái)控制元素的修改權(quán)限,適用于所有支持begin()和end()方法的容器,該循環(huán)方式不適用于未提供這些方法的C++98/03容器
    2024-09-09
  • C語(yǔ)言實(shí)現(xiàn)解析csv格式文件的示例代碼

    C語(yǔ)言實(shí)現(xiàn)解析csv格式文件的示例代碼

    CSV,有時(shí)也稱為字符分隔值,其文件以純文本形式存儲(chǔ)表格數(shù)據(jù)(數(shù)字和文本),本文為大家整理了C語(yǔ)言解析csv文件的方法,需要的可以參考一下
    2023-06-06
  • opencv 做人臉識(shí)別 opencv 人臉匹配分析

    opencv 做人臉識(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-11
  • C++?sqlite3數(shù)據(jù)庫(kù)配置使用教程

    C++?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-08
  • C++中std::string::npos的用法

    C++中std::string::npos的用法

    這篇文章主要介紹了C++中std::string::npos的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-08-08
  • C語(yǔ)言中static與sizeof查缺補(bǔ)漏篇

    C語(yǔ)言中static與sizeof查缺補(bǔ)漏篇

    static在修飾變量的時(shí)候,如果是修飾全局變量,則跟全局變量功能一樣;如果是修改局部變量,則每次調(diào)用的時(shí)候,保持著上一次的值;而sizeof是用來(lái)判斷一個(gè)變量及數(shù)據(jù)類型所占字節(jié)數(shù)的,下面我們?cè)敿?xì)來(lái)看看
    2022-07-07
  • C++操作SQLite簡(jiǎn)明教程

    C++操作SQLite簡(jiǎn)明教程

    這篇文章主要介紹了C++操作SQLite簡(jiǎn)明教程,包含創(chuàng)建表、插入數(shù)據(jù)、查詢數(shù)據(jù)等常用操作,需要的朋友可以參考下
    2014-06-06
  • C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘

    C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘

    這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-05-05

最新評(píng)論