內(nèi)存泄漏-原因、避免以及定位
作為C/C++開(kāi)發(fā)人員,內(nèi)存泄漏是最容易遇到的問(wèn)題之一,這是由C/C++語(yǔ)言的特性引起的。C/C++語(yǔ)言與其他語(yǔ)言不同,需要開(kāi)發(fā)者去申請(qǐng)和釋放內(nèi)存,即需要開(kāi)發(fā)者去管理內(nèi)存,如果內(nèi)存使用不當(dāng),就容易造成段錯(cuò)誤(segment fault)或者內(nèi)存泄漏(memory leak)。
今天,借助此文,分析下項(xiàng)目中經(jīng)常遇到的導(dǎo)致內(nèi)存泄漏的原因,以及如何避免和定位內(nèi)存泄漏。
主要內(nèi)容如下:

背景
C/C++語(yǔ)言中,內(nèi)存的分配與回收都是由開(kāi)發(fā)人員在編寫(xiě)代碼時(shí)主動(dòng)完成的,好處是內(nèi)存管理的開(kāi)銷(xiāo)較小,程序擁有更高的執(zhí)行效率;弊端是依賴(lài)于開(kāi)發(fā)者的水平,隨著代碼規(guī)模的擴(kuò)大,極容易遺漏釋放內(nèi)存的步驟,或者一些不規(guī)范的編程可能會(huì)使程序具有安全隱患。如果對(duì)內(nèi)存管理不當(dāng),可能導(dǎo)致程序中存在內(nèi)存缺陷,甚至?xí)谶\(yùn)行時(shí)產(chǎn)生內(nèi)存故障錯(cuò)誤。
內(nèi)存泄漏是各類(lèi)缺陷中十分棘手的一種,對(duì)系統(tǒng)的穩(wěn)定運(yùn)行威脅較大。當(dāng)動(dòng)態(tài)分配的內(nèi)存在程序結(jié)束之前沒(méi)有被回收時(shí),則發(fā)生了內(nèi)存泄漏。由于系統(tǒng)軟件,如操作系統(tǒng)、編譯器、開(kāi)發(fā)環(huán)境等都是由C/C++語(yǔ)言實(shí)現(xiàn)的,不可避免地存在內(nèi)存泄漏缺陷,特別是一些在服務(wù)器上長(zhǎng)期運(yùn)行的軟件,若存在內(nèi)存泄漏則會(huì)造成嚴(yán)重后果,例如性能下降、程序終止、系統(tǒng)崩潰、無(wú)法提供服務(wù)等。
所以,本文從原因、避免以及定位幾個(gè)方面去深入講解,希望能給大家?guī)?lái)幫助。
概念
內(nèi)存泄漏(Memory Leak)是指程序中己動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無(wú)法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
當(dāng)我們?cè)诔绦蛑袑?duì)原始指針(raw pointer)使用new操作符或者free函數(shù)的時(shí)候,實(shí)際上是在堆上為其分配內(nèi)存,這個(gè)內(nèi)存指的是RAM,而不是硬盤(pán)等永久存儲(chǔ)。持續(xù)申請(qǐng)而不釋放(或者少量釋放)內(nèi)存的應(yīng)用程序,最終因內(nèi)存耗盡導(dǎo)致OOM(out of memory)。

方便大家理解內(nèi)存泄漏的危害,舉個(gè)簡(jiǎn)單的例子。有一個(gè)賓館,共有100間房間,顧客每次都是在前臺(tái)進(jìn)行登記,然后拿到房間鑰匙。如果有些顧客不需要該房間了,既不去前臺(tái)處登記退房,也不歸還鑰匙,久而久之,前臺(tái)處可用房間越來(lái)越少,收入也越來(lái)越少,瀕臨倒閉。
當(dāng)程序申請(qǐng)了內(nèi)存,而不進(jìn)行歸還,久而久之,可用內(nèi)存越來(lái)越少,OS就會(huì)進(jìn)行自我保護(hù),殺掉該進(jìn)程,這就是我們常說(shuō)的OOM(out of memory)。
分類(lèi)
內(nèi)存泄漏分為以下兩類(lèi):
堆內(nèi)存泄漏:我們經(jīng)常說(shuō)的內(nèi)存泄漏就是堆內(nèi)存泄漏,在堆上申請(qǐng)了資源,在結(jié)束使用的時(shí)候,沒(méi)有釋放歸還給OS,從而導(dǎo)致該塊內(nèi)存永遠(yuǎn)不會(huì)被再次使用 資源泄漏:通常指的是系統(tǒng)資源,比如socket,文件描述符等,因?yàn)檫@些在系統(tǒng)中都是有限制的,如果創(chuàng)建了而不歸還,久而久之,就會(huì)耗盡資源,導(dǎo)致其他程序不可用
本文主要分析堆內(nèi)存泄漏,所以后面的內(nèi)存泄漏均指的是堆內(nèi)存泄漏。
根源
內(nèi)存泄漏,主要指的是在堆(heap)上申請(qǐng)的動(dòng)態(tài)內(nèi)存泄漏,或者說(shuō)是指針指向的內(nèi)存塊忘了被釋放,導(dǎo)致該塊內(nèi)存不能再被申請(qǐng)重新使用。
之前在知乎上看了一句話,指針是C的精髓,也是初學(xué)者的一個(gè)坎。換句話說(shuō),內(nèi)存管理是C的精髓,C/C++可以直接跟OS打交道,從性能角度出發(fā),開(kāi)發(fā)者可以根據(jù)自己的實(shí)際使用場(chǎng)景靈活進(jìn)行內(nèi)存分配和釋放。
雖然在C++中自C++11引入了smart pointer,雖然很大程度上能夠避免使用裸指針,但仍然不能完全避免,最重要的一個(gè)原因是你不能保證組內(nèi)其他人不適用指針,更不能保證合作部門(mén)不使用指針。
那么為什么C/C++中會(huì)存在指針呢?
這就得從進(jìn)程的內(nèi)存布局說(shuō)起。
進(jìn)程內(nèi)存布局

上圖為32位進(jìn)程的內(nèi)存布局,從上圖中主要包含以下幾個(gè)塊:
內(nèi)核空間:供內(nèi)核使用,存放的是內(nèi)核代碼和數(shù)據(jù) stack:這就是我們經(jīng)常所說(shuō)的棧,用來(lái)存儲(chǔ)自動(dòng)變量(automatic variable) mmap:也成為內(nèi)存映射,用來(lái)在進(jìn)程虛擬內(nèi)存地址空間中分配地址空間,創(chuàng)建和物理內(nèi)存的映射關(guān)系 heap:就是我們常說(shuō)的堆,動(dòng)態(tài)內(nèi)存的分配都是在堆上 bss:包含所有未初始化的全局和靜態(tài)變量,此段中的所有變量都由0或者空指針初始化,程序加載器在加載程序時(shí)為BSS段分配內(nèi)存 ds:初始化的數(shù)據(jù)塊 包含顯式初始化的全局變量和靜態(tài)變量 此段的大小由程序源代碼中值的大小決定,在運(yùn)行時(shí)不會(huì)更改 它具有讀寫(xiě)權(quán)限,因此可以在運(yùn)行時(shí)更改此段的變量值 該段可進(jìn)一步分為初始化只讀區(qū)和初始化讀寫(xiě)區(qū) text:也稱(chēng)為文本段 該段包含已編譯程序的二進(jìn)制文件。 該段是一個(gè)只讀段,用于防止程序被意外修改 該段是可共享的,因此對(duì)于文本編輯器等頻繁執(zhí)行的程序,內(nèi)存中只需要一個(gè)副本
由于本文主要講內(nèi)存分配相關(guān),所以下面的內(nèi)容僅涉及到棧(stack)和堆(heap)。

棧
棧一塊連續(xù)的內(nèi)存塊,棧上的內(nèi)存分配就是在這一塊連續(xù)內(nèi)存塊上進(jìn)行操作的。編譯器在編譯的時(shí)候,就已經(jīng)知道要分配的內(nèi)存大小,當(dāng)調(diào)用函數(shù)時(shí)候,其內(nèi)部的遍歷都會(huì)在棧上分配內(nèi)存;當(dāng)結(jié)束函數(shù)調(diào)用時(shí)候,內(nèi)部變量就會(huì)被釋放,進(jìn)而將內(nèi)存歸還給棧。
class?Object?{
??public:
????Object()?=?default;
????//?....
};
void?fun()?{
??Object?obj;
??
??//?do?sth
}
在上述代碼中,obj就是在棧上進(jìn)行分配,當(dāng)出了fun作用域的時(shí)候,會(huì)自動(dòng)調(diào)用Object的析構(gòu)函數(shù)對(duì)其進(jìn)行釋放。
前面有提到,局部變量會(huì)在作用域(如函數(shù)作用域、塊作用域等)結(jié)束后析構(gòu)、釋放內(nèi)存。因?yàn)榉峙浜歪尫诺拇涡蚴莿偤猛耆喾吹模钥捎玫蕉褩O冗M(jìn)后出(first-in-last-out, FILO)的特性,而 C++ 語(yǔ)言的實(shí)現(xiàn)一般也會(huì)使用到調(diào)用堆棧(call stack)來(lái)分配局部變量(但非標(biāo)準(zhǔn)的要求)。
因?yàn)闂I蟽?nèi)存分配和釋放,是一個(gè)進(jìn)棧和出棧的過(guò)程(對(duì)于編譯器只是一個(gè)指令),所以相比于堆上的內(nèi)存分配,棧要快的多。
雖然棧的訪問(wèn)速度要快于堆,每個(gè)線程都有一個(gè)自己的棧,棧上的對(duì)象是不能跨線程訪問(wèn)的,這就決定了棧空間大小是有限制的,如果棧空間過(guò)大,那么在大型程序中幾十乃至上百個(gè)線程,光棧空間就消耗了RAM,這就導(dǎo)致heap的可用空間變小,影響程序正常運(yùn)行。
設(shè)置
在Linux系統(tǒng)上,可用通過(guò)如下命令來(lái)查看棧大小:
ulimit?-s
10240
在筆者的機(jī)器上,執(zhí)行上述命令輸出結(jié)果是10240(KB)即10m,可以通過(guò)shell命令修改棧大小。
ulimit?-s?102400
通過(guò)如上命令,可以將棧空間臨時(shí)修改為100m,可以通過(guò)下面的命令:
/etc/security/limits.conf
分配方式
靜態(tài)分配
靜態(tài)分配由編譯器完成,假如局部變量以及函數(shù)參數(shù)等,都在編譯期就分配好了。
void?fun()?{
??int?a[10];
}
上述代碼中,a占10 * sizeof(int)個(gè)字節(jié),在編譯的時(shí)候直接計(jì)算好了,運(yùn)行的時(shí)候,直接進(jìn)棧出棧。
動(dòng)態(tài)分配
可能很多人認(rèn)為只有堆上才會(huì)存在動(dòng)態(tài)分配,在棧上只可能是靜態(tài)分配。其實(shí),這個(gè)觀點(diǎn)是錯(cuò)的,棧上也支持動(dòng)態(tài)分配,該動(dòng)態(tài)分配由alloca()函數(shù)進(jìn)行分配。棧的動(dòng)態(tài)分配和堆是不同的,通過(guò)alloca()函數(shù)分配的內(nèi)存由編譯器進(jìn)行釋放,無(wú)需手動(dòng)操作。
特點(diǎn)
分配速度快:分配大小由編譯器在編譯期完成 不會(huì)產(chǎn)生內(nèi)存碎片:棧內(nèi)存分配是連續(xù)的,以FILO的方式進(jìn)棧和出棧 大小受限:棧的大小依賴(lài)于操作系統(tǒng) 訪問(wèn)受限:只能在當(dāng)前函數(shù)或者作用域內(nèi)進(jìn)行訪問(wèn)
堆
堆(heap)是一種內(nèi)存管理方式。內(nèi)存管理對(duì)操作系統(tǒng)來(lái)說(shuō)是一件非常復(fù)雜的事情,因?yàn)槭紫葍?nèi)存容量很大,其次就是內(nèi)存需求在時(shí)間和大小塊上沒(méi)有規(guī)律(操作系統(tǒng)上運(yùn)行著幾十甚至幾百個(gè)進(jìn)程,這些進(jìn)程可能隨時(shí)都會(huì)申請(qǐng)或者是釋放內(nèi)存,并且申請(qǐng)和釋放的內(nèi)存塊大小是隨意的)。
堆這種內(nèi)存管理方式的特點(diǎn)就是自由(隨時(shí)申請(qǐng)、隨時(shí)釋放、大小塊隨意)。堆內(nèi)存是操作系統(tǒng)劃歸給堆管理器(操作系統(tǒng)中的一段代碼,屬于操作系統(tǒng)的內(nèi)存管理單元)來(lái)管理的,堆管理器提供了對(duì)應(yīng)的接口_sbrk、_mmap等,只是該接口往往由運(yùn)行時(shí)庫(kù)(Linux為glibc)進(jìn)行調(diào)用,即也可以說(shuō)由運(yùn)行時(shí)庫(kù)進(jìn)行堆內(nèi)存管理,運(yùn)行時(shí)庫(kù)提供了malloc/free函數(shù)由開(kāi)發(fā)人員調(diào)用,進(jìn)而使用堆內(nèi)存。
分配方式
正如我們所理解的那樣,由于是在運(yùn)行期進(jìn)行內(nèi)存分配,分配的大小也在運(yùn)行期才會(huì)知道,所以堆只支持動(dòng)態(tài)分配,內(nèi)存申請(qǐng)和釋放的行為由開(kāi)發(fā)者自行操作,這就很容易造成我們說(shuō)的內(nèi)存泄漏。
特點(diǎn)
變量可以在進(jìn)程范圍內(nèi)訪問(wèn),即進(jìn)程內(nèi)的所有線程都可以訪問(wèn)該變量 沒(méi)有內(nèi)存大小限制,這個(gè)其實(shí)是相對(duì)的,只是相對(duì)于棧大小來(lái)說(shuō)沒(méi)有限制,其實(shí)最終還是受限于RAM 相對(duì)棧來(lái)說(shuō)訪問(wèn)比較慢 內(nèi)存碎片 由開(kāi)發(fā)者管理內(nèi)存,即內(nèi)存的申請(qǐng)和釋放都由開(kāi)發(fā)人員來(lái)操作
堆與棧區(qū)別
理解堆和棧的區(qū)別,對(duì)我們開(kāi)發(fā)過(guò)程中會(huì)非常有用,結(jié)合上面的內(nèi)容,總結(jié)下二者的區(qū)別。
對(duì)于棧來(lái)講,是由編譯器自動(dòng)管理,無(wú)需我們手工控制;對(duì)于堆來(lái)說(shuō),釋放工作由程序員控制,容易產(chǎn)生memory leak
空間大小不同 一般來(lái)講在 32 位系統(tǒng)下,堆內(nèi)存可以達(dá)到3G的空間,從這個(gè)角度來(lái)看堆內(nèi)存幾乎是沒(méi)有什么限制的。 對(duì)于棧來(lái)講,一般都是有一定的空間大小的,一般依賴(lài)于操作系統(tǒng)(也可以人工設(shè)置) 能否產(chǎn)生碎片不同 對(duì)于堆來(lái)講,頻繁的內(nèi)存分配和釋放勢(shì)必會(huì)造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。 對(duì)于棧來(lái)講,內(nèi)存都是連續(xù)的,申請(qǐng)和釋放都是指令移動(dòng),類(lèi)似于數(shù)據(jù)結(jié)構(gòu)中的 進(jìn)棧和出棧增長(zhǎng)方向不同 對(duì)于堆來(lái)講,生長(zhǎng)方向是向上的,也就是向著內(nèi)存地址增加的方向 對(duì)于棧來(lái)講,它的生長(zhǎng)方向是向下的,是向著內(nèi)存地址減小的方向增長(zhǎng) 分配方式不同 堆都是動(dòng)態(tài)分配的,比如我們常見(jiàn)的malloc/new;而棧則有靜態(tài)分配和動(dòng)態(tài)分配兩種。 靜態(tài)分配是編譯器完成的,比如局部變量的分配,而棧的動(dòng)態(tài)分配則通過(guò)alloca()函數(shù)完成 二者動(dòng)態(tài)分配是不同的,棧的動(dòng)態(tài)分配的內(nèi)存由編譯器進(jìn)行釋放,而堆上的動(dòng)態(tài)分配的內(nèi)存則必須由開(kāi)發(fā)人自行釋放 分配效率不同 棧有操作系統(tǒng)分配專(zhuān)門(mén)的寄存器存放棧的地址,壓棧出棧都有專(zhuān)門(mén)的指令執(zhí)行,這就決定了棧的效率比較高 堆內(nèi)存的申請(qǐng)和釋放專(zhuān)門(mén)有運(yùn)行時(shí)庫(kù)提供的函數(shù),里面涉及復(fù)雜的邏輯,申請(qǐng)和釋放效率低于棧
截止到這里,棧和堆的基本特性以及各自的優(yōu)缺點(diǎn)、使用場(chǎng)景已經(jīng)分析完成,在這里給開(kāi)發(fā)者一個(gè)建議,能使用棧的時(shí)候,就盡量使用棧,一方面是因?yàn)樾矢哂诙眩硪环矫鎯?nèi)存的申請(qǐng)和釋放由編譯器完成,這樣就避免了很多問(wèn)題。
擴(kuò)展
終于到了這一小節(jié),其實(shí),上面講的那么多,都是為這一小節(jié)做鋪墊。
在前面的內(nèi)容中,我們對(duì)比了棧和堆,雖然棧效率比較高,且不存在內(nèi)存泄漏、內(nèi)存碎片等,但是由于其本身的局限性(不能多線程、大小受限),所以在很多時(shí)候,還是需要在堆上進(jìn)行內(nèi)存。
我們先看一段代碼:
#include?
#include?
int?main()?{
??int?a;
??int?*p;
??p?=?(int?*)malloc(sizeof(int));
??free(p);
??return?0;
}
上述代碼很簡(jiǎn)單,有兩個(gè)變量a和p,類(lèi)型分別為int和int *,其中,a和p存儲(chǔ)在棧上,p的值為在堆上的某塊地址(在上述代碼中,p的值為0x1c66010),上述代碼布局如下圖所示:

產(chǎn)生方式
以產(chǎn)生的方式來(lái)分類(lèi),內(nèi)存泄漏可以分為四類(lèi):
常發(fā)性內(nèi)存泄漏 偶發(fā)性內(nèi)存泄漏 一次性內(nèi)存泄漏 隱式內(nèi)存泄漏
常發(fā)性內(nèi)存泄漏
產(chǎn)生內(nèi)存泄漏的代碼或者函數(shù)會(huì)被多次執(zhí)行到,在每次執(zhí)行的時(shí)候,都會(huì)產(chǎn)生內(nèi)存泄漏。
偶發(fā)性內(nèi)存泄漏
與常發(fā)性內(nèi)存泄漏不同的是,偶發(fā)性內(nèi)存泄漏函數(shù)只在特定的場(chǎng)景下才會(huì)被執(zhí)行。
筆者在19年的時(shí)候,曾經(jīng)遇到一個(gè)這種內(nèi)存泄漏。有一個(gè)函數(shù)專(zhuān)門(mén)進(jìn)行價(jià)格加密,每次泄漏3個(gè)字節(jié),且只有在競(jìng)價(jià)成功的時(shí)候,才會(huì)調(diào)用此函數(shù)進(jìn)行價(jià)格加密,因此泄漏的非常不明顯。
當(dāng)時(shí)發(fā)現(xiàn)這個(gè)問(wèn)題,是上線后的第二天,幫忙排查線上問(wèn)題,發(fā)現(xiàn)內(nèi)存較上線前上漲了點(diǎn)(大概幾百兆的樣子),了解glibc內(nèi)存分配原理的都清楚,調(diào)用delete后,內(nèi)存不一定會(huì)歸還給OS,但是本著寧可信其有,不可信其無(wú)的心態(tài),決定來(lái)分析是否真的存在內(nèi)存泄漏。
當(dāng)時(shí)用了個(gè)比較傻瓜式的方法,通過(guò)top命令,將該進(jìn)程所占的內(nèi)存輸出到本地文件,大概幾個(gè)小時(shí)后,將這些數(shù)據(jù)導(dǎo)入Excel中,內(nèi)存占用基本呈一條斜線,所以基本能夠確定代碼存在內(nèi)存泄漏,所以就對(duì)新上線的這部分代碼進(jìn)行重新review,定位到泄漏點(diǎn),然后修復(fù),重新上線。
一次性內(nèi)存泄漏
這種內(nèi)存泄漏在程序的生命周期內(nèi)只會(huì)泄漏一次,或者說(shuō)造成泄漏的代碼只會(huì)被執(zhí)行一次。
有的時(shí)候,這種可能不算內(nèi)存泄漏,或者說(shuō)設(shè)計(jì)如此。就以筆者現(xiàn)在線上的服務(wù)來(lái)說(shuō),類(lèi)似于如下這種:
int?main()?{
??auto?*service?=?new?Service;
??//?do?sth
??service->Run();//?服務(wù)啟動(dòng)
??service->Loop();?//?可以理解為一個(gè)sleep,目的是使得程序不退出
??return?0;
}
這種嚴(yán)格意義上,并不算內(nèi)存泄漏,因?yàn)槌绦蚴沁@么設(shè)計(jì)的,即使程序異常退出,那么整個(gè)服務(wù)進(jìn)程也就退出了,當(dāng)然,在Loop()后面加個(gè)delete更好。
隱式內(nèi)存泄漏
程序在運(yùn)行過(guò)程中不停的分配內(nèi)存,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說(shuō)這里并沒(méi)有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請(qǐng)的內(nèi)存。但是對(duì)于一個(gè)服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個(gè)月,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱(chēng)這類(lèi)內(nèi)存泄漏為隱式內(nèi)存泄漏。
比較常見(jiàn)的隱式內(nèi)存泄漏有以下三種:
內(nèi)存碎片:還記得我們之前的那篇文章深入理解glibc內(nèi)存管理精髓,程序跑了幾天之后,進(jìn)程就因?yàn)镺OM導(dǎo)致了退出,就是因?yàn)閮?nèi)存碎片導(dǎo)致剩下的內(nèi)存不能被重新分配導(dǎo)致 即使我們調(diào)用了free/delete,運(yùn)行時(shí)庫(kù)不一定會(huì)將內(nèi)存歸還OS,具體深入理解glibc內(nèi)存管理精髓 用過(guò)STL的知道,STL內(nèi)部有一個(gè)自己的allocator,我們可以當(dāng)做一個(gè)memory poll,當(dāng)調(diào)用vector.clear()時(shí)候,內(nèi)存并不會(huì)歸還OS,而是放回allocator,其內(nèi)部根據(jù)一定的策略,在特定的時(shí)候?qū)?nèi)存歸還OS,是不是跟glibc原理很像??
分類(lèi)
未釋放
這種是很常見(jiàn)的,比如下面的代碼:
int?fun()?{
????char?*?pBuffer?=?malloc(sizeof(char));
????
????/*?Do?some?work?*/
????return?0;
}
上面代碼是非常常見(jiàn)的內(nèi)存泄漏場(chǎng)景(也可以使用new來(lái)進(jìn)行分配),我們申請(qǐng)了一塊內(nèi)存,但是在fun函數(shù)結(jié)束時(shí)候沒(méi)有調(diào)用free函數(shù)進(jìn)行內(nèi)存釋放。
在C++開(kāi)發(fā)中,還有一種內(nèi)存泄漏,如下:
class?Obj?{
?public:
???Obj(int?size)?{
?????buffer_?=?new?char;
???}
???~Obj(){}
??private:
???char?*buffer_;
};
int?fun()?{
??Object?obj;
??//?do?sth
??return?0;
}
上面這段代碼中,析構(gòu)函數(shù)沒(méi)有釋放成員變量buffer_指向的內(nèi)存,所以在編寫(xiě)析構(gòu)函數(shù)的時(shí)候,一定要仔細(xì)分析成員變量有沒(méi)有申請(qǐng)動(dòng)態(tài)內(nèi)存,如果有,則需要手動(dòng)釋放,我們重新編寫(xiě)了析構(gòu)函數(shù),如下:
~Object()?{
??delete?buffer_;
}
在C/C++中,對(duì)于普通函數(shù),如果申請(qǐng)了堆資源,請(qǐng)跟進(jìn)代碼的具體場(chǎng)景調(diào)用free/delete進(jìn)行資源釋放;對(duì)于class,如果申請(qǐng)了堆資源,則需要在對(duì)應(yīng)的析構(gòu)函數(shù)中調(diào)用free/delete進(jìn)行資源釋放。
未匹配
在C++中,我們經(jīng)常使用new操作符來(lái)進(jìn)行內(nèi)存分配,其內(nèi)部主要做了兩件事:
通過(guò)operator new從堆上申請(qǐng)內(nèi)存(glibc下,operator new底層調(diào)用的是malloc) 調(diào)用構(gòu)造函數(shù)(如果操作對(duì)象是一個(gè)class的話)
對(duì)應(yīng)的,使用delete操作符來(lái)釋放內(nèi)存,其順序正好與new相反:
調(diào)用對(duì)象的析構(gòu)函數(shù)(如果操作對(duì)象是一個(gè)class的話) 通過(guò)operator delete釋放內(nèi)存
void*?operator?new(std::size_t?size)?{
????void*?p?=?malloc(size);
????if?(p?==?nullptr)?{
????????throw("new?failed?to?allocate?%zu?bytes",?size);
????}
????return?p;
}
void*?operator?new[](std::size_t?size)?{
????void*?p?=?malloc(size);
????if?(p?==?nullptr)?{
????????throw("new[]?failed?to?allocate?%zu?bytes",?size);
????}
????return?p;
}
void??operator?delete(void*?ptr)?throw()?{
????free(ptr);
}
void??operator?delete[](void*?ptr)?throw()?{
????free(ptr);
}
為了加深多這塊的理解,我們舉個(gè)例子:
class?Test?{
?public:
???Test()?{
?????std::cout?<"in?Test"?<std::endl;
???}
???//?other
???~Test()?{
?????std::cout?<"in?~Test"?<std::endl;
???}
};
int?main()?{
??Test?*t?=?new?Test;
??//?do?sth
??delete?t;
??return?0;
}
在上述main函數(shù)中,我們使用new 操作符創(chuàng)建一個(gè)Test類(lèi)指針
通過(guò)operator new申請(qǐng)內(nèi)存(底層malloc實(shí)現(xiàn)) 通過(guò)placement new在上述申請(qǐng)的內(nèi)存塊上調(diào)用構(gòu)造函數(shù) 調(diào)用ptr->~Test()釋放Test對(duì)象的成員變量 調(diào)用operator delete釋放內(nèi)存
上述過(guò)程,可以理解為如下:
//?new
void?*ptr?=?malloc(sizeof(Test));
t?=?new(ptr)Test
??
//?delete
ptr->~Test();
free(ptr);
好了,上述內(nèi)容,我們簡(jiǎn)單的講解了C++中new和delete操作符的基本實(shí)現(xiàn)以及邏輯,那么,我們就簡(jiǎn)單總結(jié)下下產(chǎn)生內(nèi)存泄漏的幾種類(lèi)型。
new 和 free
仍然以上面的Test對(duì)象為例,代碼如下:
Test?*t?=?new?Test;
free(t)
此處會(huì)產(chǎn)生內(nèi)存泄漏,在上面,我們已經(jīng)分析過(guò),new操作符會(huì)先通過(guò)operator new分配一塊內(nèi)存,然后在該塊內(nèi)存上調(diào)用placement new即調(diào)用Test的構(gòu)造函數(shù)。而在上述代碼中,只是通過(guò)free函數(shù)釋放了內(nèi)存,但是沒(méi)有調(diào)用Test的析構(gòu)函數(shù)以釋放Test的成員變量,從而引起內(nèi)存泄漏。
new[] 和 delete
int?main()?{
??Test?*t?=?new?Test?[10];
??//?do?sth
??delete?t;
??return?0;
}
在上述代碼中,我們通過(guò)new創(chuàng)建了一個(gè)Test類(lèi)型的數(shù)組,然后通delete操作符刪除該數(shù)組,編譯并執(zhí)行,輸出如下:
in?Test
in?Test
in?Test
in?Test
in?Test
in?Test
in?Test
in?Test
in?Test
in?Test
in?~Test
從上面輸出結(jié)果可以看出,調(diào)用了10次構(gòu)造函數(shù),但是只調(diào)用了一次析構(gòu)函數(shù),所以引起了內(nèi)存泄漏。這是因?yàn)檎{(diào)用delete t釋放了通過(guò)operator new[]申請(qǐng)的內(nèi)存,即malloc申請(qǐng)的內(nèi)存塊,且只調(diào)用了t[0]對(duì)象的析構(gòu)函數(shù),t[1..9]對(duì)象的析構(gòu)函數(shù)并沒(méi)有被調(diào)用。
虛析構(gòu)
記得08年面谷歌的時(shí)候,有一道題,面試官問(wèn),std::string能否被繼承,為什么?
當(dāng)時(shí)沒(méi)回答上來(lái),后來(lái)過(guò)了沒(méi)多久,進(jìn)行面試復(fù)盤(pán)的時(shí)候,偶然看到繼承需要父類(lèi)析構(gòu)函數(shù)為virtual,才恍然大悟,原來(lái)考察點(diǎn)在這塊。
下面我們看下std::string的析構(gòu)函數(shù)定義:
~basic_string()?{?
??_M_rep()->_M_dispose(this->get_allocator());?
}
這塊需要特別說(shuō)明下,std::basic_string是一個(gè)模板,而std::string是該模板的一個(gè)特化,即std::basic_string。
typedef?std::basic_string<char>?string;
現(xiàn)在我們可以給出這個(gè)問(wèn)題的答案:不能,因?yàn)閟td::string的析構(gòu)函數(shù)不為virtual,這樣會(huì)引起內(nèi)存泄漏。
仍然以一個(gè)例子來(lái)進(jìn)行證明。
class?Base?{
?public:
??Base(){
????buffer_?=?new?char[10];
??}
??~Base()?{
????std::cout?<"in?Base::~Base"?<std::endl;
????delete?[]buffer_;
??}
private:
??char?*buffer_;
};
class?Derived?:?public?Base?{
?public:
??Derived(){}
??~Derived()?{
????std::cout?<"int?Derived::~Derived"?<std::endl;
??}
};
int?main()?{
??Base?*base?=?new?Derived;
??delete?base;
??return?0;
}
上面代碼輸出如下:
in?Base::~Base
可見(jiàn),上述代碼并沒(méi)有調(diào)用派生類(lèi)Derived的析構(gòu)函數(shù),如果派生類(lèi)中在堆上申請(qǐng)了資源,那么就會(huì)產(chǎn)生內(nèi)存泄漏。
為了避免因?yàn)槔^承導(dǎo)致的內(nèi)存泄漏,我們需要將父類(lèi)的析構(gòu)函數(shù)聲明為virtual,代碼如下(只列了部分修改代碼,其他不變):
~Base()?{
????std::cout?<"in?Base::~Base"?<std::endl;
????delete?[]buffer_;
??}
然后重新執(zhí)行代碼,輸出結(jié)果如下:
int?Derived::~Derived
in?Base::~Base
借助此文,我們?cè)俅慰偨Y(jié)下存在繼承情況下,構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用順序。
派生類(lèi)對(duì)象在創(chuàng)建時(shí)構(gòu)造函數(shù)調(diào)用順序:
調(diào)用父類(lèi)的構(gòu)造函數(shù) 調(diào)用父類(lèi)成員變量的構(gòu)造函數(shù) 調(diào)用派生類(lèi)本身的構(gòu)造函數(shù)
派生類(lèi)對(duì)象在析構(gòu)時(shí)的析構(gòu)函數(shù)調(diào)用順序:
執(zhí)行派生類(lèi)自身的析構(gòu)函數(shù) 執(zhí)行派生類(lèi)成員變量的析構(gòu)函數(shù) 執(zhí)行父類(lèi)的析構(gòu)函數(shù)
為了避免存在繼承關(guān)系時(shí)候的內(nèi)存泄漏,請(qǐng)遵守一條規(guī)則:無(wú)論派生類(lèi)有沒(méi)有申請(qǐng)堆上的資源,請(qǐng)將父類(lèi)的析構(gòu)函數(shù)聲明為virtual。
循環(huán)引用
在C++開(kāi)發(fā)中,為了盡可能的避免內(nèi)存泄漏,自C++11起引入了smart pointer,常見(jiàn)的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已經(jīng)被廢棄),其中weak_ptr是為了解決循環(huán)引用而存在,其往往與shared_ptr結(jié)合使用。
下面,我們看一段代碼:
class?Controller?{
?public:
??Controller()?=?default;
??~Controller()?{
????std::cout?<"in?~Controller"?<std::endl;
??}
??class?SubController?{
???public:
????SubController()?=?default;
????~SubController()?{
??????std::cout?<"in?~SubController"?<std::endl;
????}
????std::shared_ptr?controller_;
??};
??std::shared_ptr?sub_controller_;
};
int?main()?{
??auto?controller?=?std::make_shared();
??auto?sub_controller?=?std::make_shared();
??controller->sub_controller_?=?sub_controller;
??sub_controller->controller_?=?controller;
??return?0;
}
編譯并執(zhí)行上述代碼,發(fā)現(xiàn)并沒(méi)有調(diào)用Controller和SubController的析構(gòu)函數(shù),我們嘗試著打印下引用計(jì)數(shù),代碼如下:
int?main()?{
??auto?controller?=?std::make_shared();
??auto?sub_controller?=?std::make_shared();
??controller->sub_controller_?=?sub_controller;
??sub_controller->controller_?=?controller;
??std::cout?<"controller?use_count:?"?<std::endl;
??std::cout?<"sub_controller?use_count:?"?<std::endl;
??return?0;
}
編譯并執(zhí)行之后,輸出如下:
controller?use_count:?2
sub_controller?use_count:?2
通過(guò)上面輸出可以發(fā)現(xiàn),因?yàn)橐糜?jì)數(shù)都是2,所以在main函數(shù)結(jié)束的時(shí)候,不會(huì)調(diào)用controller和sub_controller的析構(gòu)函數(shù),所以就出現(xiàn)了內(nèi)存泄漏。
上面產(chǎn)生內(nèi)存泄漏的原因,就是我們常說(shuō)的循環(huán)引用。

為了解決std::shared_ptr循環(huán)引用導(dǎo)致的內(nèi)存泄漏,我們可以使用std::weak_ptr來(lái)單面去除上圖中的循環(huán)。
class?Controller?{
?public:
??Controller()?=?default;
??~Controller()?{
????std::cout?<"in?~Controller"?<std::endl;
??}
??class?SubController?{
???public:
????SubController()?=?default;
????~SubController()?{
??????std::cout?<"in?~SubController"?<std::endl;
????}
????std::weak_ptr?controller_;
??};
??std::shared_ptr?sub_controller_;
};
在上述代碼中,我們將SubController類(lèi)中controller_的類(lèi)型從std::shared_ptr變成std::weak_ptr,重新編譯執(zhí)行,結(jié)果如下:
controller?use_count:?1
sub_controller?use_count:?2
in?~Controller
in?~SubController
從上面結(jié)果可以看出,controller和sub_controller均以釋放,所以循環(huán)引用引起的內(nèi)存泄漏問(wèn)題,也得以解決。

可能有人會(huì)問(wèn),使用std::shared_ptr可以直接訪問(wèn)對(duì)應(yīng)的成員函數(shù),如果是std::weak_ptr的話,怎么訪問(wèn)呢?我們可以使用下面的方式:
std::shared_ptr?controller?=?controller_.lock();
即在子類(lèi)SubController中,如果要使用controller調(diào)用其對(duì)應(yīng)的函數(shù),就可以使用上面的方式。
避免
避免在堆上分配
眾所周知,大部分的內(nèi)存泄漏都是因?yàn)樵诙焉戏峙湟鸬模绻覀儾辉诙焉线M(jìn)行分配,就不會(huì)存在內(nèi)存泄漏了(這不廢話嘛),我們可以根據(jù)具體的使用場(chǎng)景,如果對(duì)象可以在棧上進(jìn)行分配,就在棧上進(jìn)行分配,一方面棧的效率遠(yuǎn)高于堆,另一方面,還能避免內(nèi)存泄漏,我們何樂(lè)而不為呢。
手動(dòng)釋放
對(duì)于malloc函數(shù)分配的內(nèi)存,在結(jié)束使用的時(shí)候,使用free函數(shù)進(jìn)行釋放 對(duì)于new操作符創(chuàng)建的對(duì)象,切記使用delete來(lái)進(jìn)行釋放 對(duì)于new []創(chuàng)建的對(duì)象,使用delete[]來(lái)進(jìn)行釋放(使用free或者delete均會(huì)造成內(nèi)存泄漏)
避免使用裸指針
盡可能避免使用裸指針,除非所調(diào)用的lib庫(kù)或者合作部門(mén)的接口是裸指針。
int?fun(int?*ptr)?{//?fun?是一個(gè)接口或lib函數(shù)
??//?do?sth
??
??return?0;
}
int?main()?{}
??int?a?=?1000;
??int?*ptr?=?&a;
??//?...
??fun(ptr);
??
??return?0;
}
在上面的fun函數(shù)中,有一個(gè)參數(shù)ptr,為int *,我們需要根據(jù)上下文來(lái)分析這個(gè)指針是否需要釋放,這是一種很不好的設(shè)計(jì)
使用STL中或者自己實(shí)現(xiàn)對(duì)象
在C++中,提供了相對(duì)完善且可靠的STL供我們使用,所以能用STL的盡可能的避免使用C中的編程方式,比如:
使用std::string 替代char *, string類(lèi)自己會(huì)進(jìn)行內(nèi)存管理,而且優(yōu)化的相當(dāng)不錯(cuò) 使用std::vector或者std::array來(lái)替代傳統(tǒng)的數(shù)組 其它適合使用場(chǎng)景的對(duì)象
智能指針
自C++11開(kāi)始,STL中引入了智能指針(smart pointer)來(lái)動(dòng)態(tài)管理資源,針對(duì)使用場(chǎng)景的不同,提供了以下三種智能指針。
unique_ptr
unique_ptr是限制最嚴(yán)格的一種智能指針,用來(lái)替代之前的auto_ptr,獨(dú)享被管理對(duì)象指針?biāo)袡?quán)。當(dāng)unique_ptr對(duì)象被銷(xiāo)毀時(shí),會(huì)在其析構(gòu)函數(shù)內(nèi)刪除關(guān)聯(lián)的原始指針。
unique_ptr對(duì)象分為以下兩類(lèi):
unique_ptr
該類(lèi)型的對(duì)象關(guān)聯(lián)了單個(gè)Type類(lèi)型的指針 std::unique_ptr???p1(new?Type) ;?//?c++11
auto?p1?=?std::make_unique();?//?c++14 unique_ptr
該類(lèi)型的對(duì)象關(guān)聯(lián)了多個(gè)Type類(lèi)型指針,即一個(gè)對(duì)象數(shù)組 std::unique_ptr?p2(new?Type[n]()) ;?//?c++11
auto?p2?=?std::make_unique(n);?//?c++14 不可用被復(fù)制
unique_ptr<int>?a(new?int(0));
unique_ptr<int>?b?=?a;??//?編譯錯(cuò)誤
unique_ptr<int>?b?=?std::move(a);?//?可以通過(guò)move語(yǔ)義進(jìn)行所有權(quán)轉(zhuǎn)移
根據(jù)使用場(chǎng)景,可以使用std::unique_ptr來(lái)避免內(nèi)存泄漏,如下:
void?fun()?{
??unique_ptr<int>?a(new?int(0));
??//?use?a
}
在上述fun函數(shù)結(jié)束的時(shí)候,會(huì)自動(dòng)調(diào)用a的析構(gòu)函數(shù),從而釋放其關(guān)聯(lián)的指針。
shared_ptr
與unique_ptr不同的是,unique_ptr是獨(dú)占管理權(quán),而shared_ptr則是共享管理權(quán),即多個(gè)shared_ptr可以共用同一塊關(guān)聯(lián)對(duì)象,其內(nèi)部采用的是引用計(jì)數(shù),在拷貝的時(shí)候,引用計(jì)數(shù)+1,而在某個(gè)對(duì)象退出作用域或者釋放的時(shí)候,引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)為0的時(shí)候,會(huì)自動(dòng)釋放其管理的對(duì)象。
void?fun()?{
??std::shared_ptr?a;?//?a是一個(gè)空對(duì)象
??{
????std::shared_ptr?b?=?std::make_shared();?//?分配資源
????a?=?b;?//?此時(shí)引用計(jì)數(shù)為2
????{
??????std::shared_ptr?c?=?a;?//?此時(shí)引用計(jì)數(shù)為3
????}?//?c退出作用域,此時(shí)引用計(jì)數(shù)為2
??}?//?b?退出作用域,此時(shí)引用計(jì)數(shù)為1
}?//?a?退出作用域,引用計(jì)數(shù)為0,釋放對(duì)象
weak_ptr
weak_ptr的出現(xiàn),主要是為了解決shared_ptr的循環(huán)引用,其主要是與shared_ptr一起來(lái)私用。和shared_ptr不同的地方在于,其并不會(huì)擁有資源,也就是說(shuō)不能訪問(wèn)對(duì)象所提供的成員函數(shù),不過(guò),可以通過(guò)weak_ptr.lock()來(lái)產(chǎn)生一個(gè)擁有訪問(wèn)權(quán)限的shared_ptr。
std::weak_ptr?a;
{
??std::shared_ptr?b?=?std::make_shared();
??a?=?b
}?//?b所對(duì)應(yīng)的資源釋放
RAII
RAII是Resource Acquisition is Initialization(資源獲取即初始化)的縮寫(xiě),是C++語(yǔ)言的一種管理資源,避免泄漏的用法。
利用的就是C++構(gòu)造的對(duì)象最終會(huì)被銷(xiāo)毀的原則。利用C++對(duì)象生命周期的概念來(lái)控制程序的資源,比如內(nèi)存,文件句柄,網(wǎng)絡(luò)連接等。
RAII的做法是使用一個(gè)對(duì)象,在其構(gòu)造時(shí)獲取對(duì)應(yīng)的資源,在對(duì)象生命周期內(nèi)控制對(duì)資源的訪問(wèn),使之始終保持有效,最后在對(duì)象析構(gòu)的時(shí)候,釋放構(gòu)造時(shí)獲取的資源。
簡(jiǎn)單地說(shuō),就是把資源的使用限制在對(duì)象的生命周期之中,自動(dòng)釋放。
舉個(gè)簡(jiǎn)單的例子,通常在多線程編程的時(shí)候,都會(huì)用到std::mutex,如下代碼:
std::mutex?mutex_;
void?fun()?{
??mutex_.lock();
??
??if?(...)?{
????mutex_.unlock();
????return;
??}
??
??mutex_.unlock()
}
在上述代碼中,如果if分支多的話,每個(gè)if分支里面都要釋放鎖,如果一不小心忘記釋放,那么就會(huì)造成故障,為了解決這個(gè)問(wèn)題,我們使用RAII技術(shù),代碼如下:
std::mutex?mutex_;
void?fun()?{
??std::lock_guard<std::mutex>?guard(mutex_);
??if?(...)?{
????return;
??}
}
在guard出了fun作用域的時(shí)候,會(huì)自動(dòng)調(diào)用mutex_.lock()進(jìn)行釋放,避免了很多不必要的問(wèn)題。
定位
在發(fā)現(xiàn)程序存在內(nèi)存泄漏后,往往需要定位泄漏點(diǎn),而定位這一步往往是最困難的,所以經(jīng)常為了定位泄漏點(diǎn),采取各種各樣的方案,甭管方案優(yōu)雅與否,畢竟管他白貓黑貓,抓住老鼠才是好貓,所以在本節(jié),簡(jiǎn)單說(shuō)下筆者這么多年定位泄漏點(diǎn)的方案,有些比較邪門(mén)歪道,您就隨便看看就行??。
日志
這種方案的核心思想,就是在每次分配內(nèi)存的時(shí)候,打印指針地址,在釋放內(nèi)存的時(shí)候,打印內(nèi)存地址,這樣在程序結(jié)束的時(shí)候,通過(guò)分配和釋放的差,如果分配的條數(shù)大于釋放的條數(shù),那么基本就能確定程序存在內(nèi)存泄漏,然后根據(jù)日志進(jìn)行詳細(xì)分析和定位。
char?*?fun()?{
??char?*p?=?(char*)malloc(20);
??printf("%s,?%d,?address?is:?%p",?__FILE__,?__LINE__,?p);
??//?do?sth
??return?p;
}
int?main()?{
??fun();
??
??return?0;
}
統(tǒng)計(jì)
統(tǒng)計(jì)方案可以理解為日志方案的一種特殊實(shí)現(xiàn),其主要原理是在分配的時(shí)候,統(tǒng)計(jì)分配次數(shù),在釋放的時(shí)候,則是統(tǒng)計(jì)釋放的次數(shù),這樣在程序結(jié)束前判斷這倆值是否一致,就能判斷出是否存在內(nèi)存泄漏。
此方法可幫助跟蹤已分配內(nèi)存的狀態(tài)。為了實(shí)現(xiàn)這個(gè)方案,需要?jiǎng)?chuàng)建三個(gè)自定義函數(shù),一個(gè)用于內(nèi)存分配,第二個(gè)用于內(nèi)存釋放,最后一個(gè)用于檢查內(nèi)存泄漏。代碼如下:
static?unsigned?int?allocated??=?0;
static?unsigned?int?deallocated??=?0;
void?*Memory_Allocate?(size_t?size)
{
????void?*ptr?=?NULL;
????ptr?=?malloc(size);
????if?(NULL?!=?ptr)?{
????????++allocated;
????}?else?{
????????//Log?error
????}
????return?ptr;
}
void?Memory_Deallocate?(void?*ptr)?{
????if(pvHandle?!=?NULL)?{
????????free(ptr);
????????++deallocated;
????}
}
int?Check_Memory_Leak(void)?{
????int?ret?=?0;
????if?(allocated?!=?deallocated)?{
????????//Log?error
????????ret?=?MEMORY_LEAK;
????}?else?{
????????ret?=?OK;
????}
????return?ret;
}
工具
在Linux上比較常用的內(nèi)存泄漏檢測(cè)工具是valgrind,所以咱們就以valgrind為工具,進(jìn)行檢測(cè)。
我們首先看一段代碼:
#include?
void?func?(void){
????char?*buff?=?(char*)malloc(10);
}
int?main?(void){
????func();?//?產(chǎn)生內(nèi)存泄漏
????return?0;
}
通過(guò) gcc -g leak.c -o leak命令進(jìn)行編譯執(zhí)行 valgrind --leak-check=full ./leak
在上述的命令執(zhí)行后,會(huì)輸出如下:
==9652==?Memcheck,?a?memory?error?detector
==9652==?Copyright?(C)?2002-2017,?and?GNU?GPL'd,?by?Julian?Seward?et?al.
==9652==?Using?Valgrind-3.15.0?and?LibVEX;?rerun?with?-h?for?copyright?info
==9652==?Command:?./leak
==9652==
==9652==
==9652==?HEAP?SUMMARY:
==9652==?????in?use?at?exit:?10?bytes?in?1?blocks
==9652==???total?heap?usage:?1?allocs,?0?frees,?10?bytes?allocated
==9652==
==9652==?10?bytes?in?1?blocks?are?definitely?lost?in?loss?record?1?of?1
==9652==????at?0x4C29F73:?malloc?(vg_replace_malloc.c:309)
==9652==????by?0x40052E:?func?(leak.c:4)
==9652==????by?0x40053D:?main?(leak.c:8)
==9652==
==9652==?LEAK?SUMMARY:
==9652==????definitely?lost:?10?bytes?in?1?blocks
==9652==????indirectly?lost:?0?bytes?in?0?blocks
==9652==??????possibly?lost:?0?bytes?in?0?blocks
==9652==????still?reachable:?0?bytes?in?0?blocks
==9652==?????????suppressed:?0?bytes?in?0?blocks
==9652==
==9652==?For?lists?of?detected?and?suppressed?errors,?rerun?with:?-s
==9652==?ERROR?SUMMARY:?1?errors?from?1?contexts?(suppressed:?0?from?0)
valgrind的檢測(cè)信息將內(nèi)存泄漏分為如下幾類(lèi):
definitely lost:確定產(chǎn)生內(nèi)存泄漏 indirectly lost:間接產(chǎn)生內(nèi)存泄漏 possibly lost:可能存在內(nèi)存泄漏 still reachable:即使在程序結(jié)束時(shí)候,仍然有指針在指向該塊內(nèi)存,常見(jiàn)于全局變量
主要上面輸出的下面幾句:
==9652==????by?0x40052E:?func?(leak.c:4)
==9652==????by?0x40053D:?main?(leak.c:8)
提示在main函數(shù)(leak.c的第8行)fun函數(shù)(leak.c的第四行)產(chǎn)生了內(nèi)存泄漏,通過(guò)分析代碼,原因定位,問(wèn)題解決。
valgrind不僅可以檢測(cè)內(nèi)存泄漏,還有其他很強(qiáng)大的功能,由于本文以內(nèi)存泄漏為主,所以其他的功能就不在此贅述了,有興趣的可以通過(guò)valgrind --help來(lái)進(jìn)行查看
?對(duì)于Windows下的內(nèi)存泄漏檢測(cè)工具,筆者推薦一款輕量級(jí)功能卻非常強(qiáng)大的工具
?UMDH,筆者在十二年前,曾經(jīng)在某外企負(fù)責(zé)內(nèi)存泄漏,代碼量幾百萬(wàn)行,光編譯就需要兩個(gè)小時(shí),嘗試了各種工具(免費(fèi)的和收費(fèi)的),最終發(fā)現(xiàn)了UMDH,如果你在Windows上進(jìn)行開(kāi)發(fā),強(qiáng)烈推薦。
經(jīng)驗(yàn)之談
在C/C++開(kāi)發(fā)過(guò)程中,內(nèi)存泄漏是一個(gè)非常常見(jiàn)的問(wèn)題,其影響相對(duì)來(lái)說(shuō)遠(yuǎn)低于coredump等,所以遇到內(nèi)存泄漏的時(shí)候,不用過(guò)于著急,大不了重啟嘛??。
在開(kāi)發(fā)過(guò)程中遵守下面的規(guī)則,基本能90+%避免內(nèi)存泄漏:
良好的編程習(xí)慣,只有有malloc/new,就得有free/delete 盡可能的使用智能指針,智能指針就是為了解決內(nèi)存泄漏而產(chǎn)生 使用log進(jìn)行記錄 也是最重要的一點(diǎn), 誰(shuí)申請(qǐng),誰(shuí)釋放
對(duì)于malloc分配內(nèi)存,分配失敗的時(shí)候返回值為NULL,此時(shí)程序可以直接退出了,而對(duì)于new進(jìn)行內(nèi)存分配,其分配失敗的時(shí)候,是拋出std::bad_alloc,所以為了第一時(shí)間發(fā)現(xiàn)問(wèn)題,不要對(duì)new異常進(jìn)行catch,畢竟內(nèi)存都分配失敗了,程序也沒(méi)有運(yùn)行的必要了。
如果我們上線后,發(fā)現(xiàn)程序存在內(nèi)存泄漏,如果不嚴(yán)重的話,可以先暫時(shí)不管線上,同時(shí)進(jìn)行排查定位;如果線上泄漏比較嚴(yán)重,那么第一時(shí)間根據(jù)實(shí)際情況來(lái)決定是否回滾。在定位問(wèn)題點(diǎn)的時(shí)候,可以采用縮小范圍法,著重分析這次新增的代碼,這樣能夠有效縮短問(wèn)題解決的時(shí)間。
結(jié)語(yǔ)
C/C++之所以復(fù)雜、效率高,是因?yàn)槠潇`活性,可用直接訪問(wèn)操作系統(tǒng)API,而正因?yàn)槠潇`活性,就很容易出問(wèn)題,團(tuán)隊(duì)成員必須愿意按照一定的規(guī)則來(lái)進(jìn)行開(kāi)發(fā),有完整的review機(jī)制,將問(wèn)題暴露在上線之前。
這樣才可以把經(jīng)歷放在業(yè)務(wù)本身,而不是查找這些問(wèn)題上,有時(shí)候往往一個(gè)小問(wèn)題就能消耗很久的時(shí)間去定位解決,所以,一定要有一個(gè)良好的開(kāi)發(fā)習(xí)慣。
參考
https://developers.redhat.com/blog/2021/05/05/memory-error-checking-in-c-and-c-comparing-sanitizers-and-valgrind
https://aticleworld.com/what-is-memory-leak-in-c-c-how-can-we-avoid/
https://iq.opengenus.org/memory-leak-in-cpp-and-how-to-avoid-it/
https://blog.nelhage.com/post/three-kinds-of-leaks/#type-1-unreachable-allocations
https://owasp.org/www-community/vulnerabilities/Memory_leak
https://www.usna.edu/Users/cs/roche/courses/s19ic221/lab05.html
https://stackoverflow.com/questions/6261201/how-to-find-memory-leak-in-a-c-code-project
- EOF -
關(guān)注『CPP開(kāi)發(fā)者』
看精選C/C++技術(shù)文章?
點(diǎn)贊和在看就是最大的支持??



