<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          內(nèi)存泄漏-原因、避免以及定位

          共 7059字,需瀏覽 15分鐘

           ·

          2022-05-31 04:23

          作為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)部主要做了兩件事:

          1. 通過(guò)operator new從堆上申請(qǐng)內(nèi)存(glibc下,operator new底層調(diào)用的是malloc)
          2. 調(diào)用構(gòu)造函數(shù)(如果操作對(duì)象是一個(gè)class的話)

          對(duì)應(yīng)的,使用delete操作符來(lái)釋放內(nèi)存,其順序正好與new相反:

          1. 調(diào)用對(duì)象的析構(gòu)函數(shù)(如果操作對(duì)象是一個(gè)class的話)
          2. 通過(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)指針

          1. 通過(guò)operator new申請(qǐng)內(nèi)存(底層malloc實(shí)現(xiàn))
          2. 通過(guò)placement new在上述申請(qǐng)的內(nèi)存塊上調(diào)用構(gòu)造函數(shù)
          3. 調(diào)用ptr->~Test()釋放Test對(duì)象的成員變量
          4. 調(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)用順序:

          1. 調(diào)用父類(lèi)的構(gòu)造函數(shù)
          2. 調(diào)用父類(lèi)成員變量的構(gòu)造函數(shù)
          3. 調(diào)用派生類(lèi)本身的構(gòu)造函數(shù)

          派生類(lèi)對(duì)象在析構(gòu)時(shí)的析構(gòu)函數(shù)調(diào)用順序:

          1. 執(zhí)行派生類(lèi)自身的析構(gòu)函數(shù)
          2. 執(zhí)行派生類(lèi)成員變量的析構(gòu)函數(shù)
          3. 執(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

          RAIIResource 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 -


          加主頁(yè)君微信,不僅C/C++技能+1

          主頁(yè)君日常還會(huì)在個(gè)人微信分享C/C++開(kāi)發(fā)學(xué)習(xí)資源技術(shù)文章精選,不定期分享一些有意思的活動(dòng)崗位內(nèi)推以及如何用技術(shù)做業(yè)余項(xiàng)目

          加個(gè)微信,打開(kāi)一扇窗


          推薦閱讀??點(diǎn)擊標(biāo)題可跳轉(zhuǎn)

          1、30 張圖帶你領(lǐng)略 glibc 內(nèi)存管理精髓

          2、從 MMU 看內(nèi)存管理

          3、明明還有大量?jī)?nèi)存,為啥報(bào)錯(cuò) “無(wú)法分配內(nèi)存” ?


          關(guān)注『CPP開(kāi)發(fā)者』

          看精選C/C++技術(shù)文章?

          點(diǎn)贊和在看就是最大的支持??

          瀏覽 23
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  a在线播放 | 欧美性爱大香蕉 | 日本一道码高清无码 | 欧美成人激情视频 | 久久免费丝袜足交视频 |