国产精品久久久aaaa,日日干夜夜操天天插,亚洲乱熟女香蕉一区二区三区少妇,99精品国产高清一区二区三区,国产成人精品一区二区色戒,久久久国产精品成人免费,亚洲精品毛片久久久久,99久久婷婷国产综合精品电影,国产一区二区三区任你鲁

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

經常遇到的導致內存泄漏的原因

Linux愛好者 ? 來源:Linux愛好者 ? 作者:Linux愛好者 ? 2022-06-06 14:12 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

作為C/C++開發人員,內存泄漏是最容易遇到的問題之一,這是由C/C++語言的特性引起的。C/C++語言與其他語言不同,需要開發者去申請和釋放內存,即需要開發者去管理內存,如果內存使用不當,就容易造成段錯誤(segment fault)或者內存泄漏(memory leak)

今天,借助此文,分析下項目中經常遇到的導致內存泄漏的原因,以及如何避免和定位內存泄漏。

主要內容如下:

ebc0584e-e54c-11ec-ba43-dac502259ad0.png

背景

C/C++語言中,內存的分配與回收都是由開發人員在編寫代碼時主動完成的,好處是內存管理的開銷較小,程序擁有更高的執行效率;弊端是依賴于開發者的水平,隨著代碼規模的擴大,極容易遺漏釋放內存的步驟,或者一些不規范的編程可能會使程序具有安全隱患。如果對內存管理不當,可能導致程序中存在內存缺陷,甚至會在運行時產生內存故障錯誤。

內存泄漏是各類缺陷中十分棘手的一種,對系統的穩定運行威脅較大。當動態分配的內存在程序結束之前沒有被回收時,則發生了內存泄漏。由于系統軟件,如操作系統、編譯器、開發環境等都是由C/C++語言實現的,不可避免地存在內存泄漏缺陷,特別是一些在服務器上長期運行的軟件,若存在內存泄漏則會造成嚴重后果,例如性能下降、程序終止、系統崩潰、無法提供服務等。

所以,本文從原因避免以及定位幾個方面去深入講解,希望能給大家帶來幫助。

概念

內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。

當我們在程序中對原始指針(raw pointer)使用new操作符或者free函數的時候,實際上是在堆上為其分配內存,這個內存指的是RAM,而不是硬盤等永久存儲。持續申請而不釋放(或者少量釋放)內存的應用程序,最終因內存耗盡導致OOM(out of memory)

ebf8d7aa-e54c-11ec-ba43-dac502259ad0.png

方便大家理解內存泄漏的危害,舉個簡單的例子。有一個賓館,共有100間房間,顧客每次都是在前臺進行登記,然后拿到房間鑰匙。如果有些顧客不需要該房間了,既不去前臺處登記退房,也不歸還鑰匙,久而久之,前臺處可用房間越來越少,收入也越來越少,瀕臨倒閉。

當程序申請了內存,而不進行歸還,久而久之,可用內存越來越少,OS就會進行自我保護,殺掉該進程,這就是我們常說的OOM(out of memory)

分類

內存泄漏分為以下兩類:

  • 堆內存泄漏:我們經常說的內存泄漏就是堆內存泄漏,在堆上申請了資源,在結束使用的時候,沒有釋放歸還給OS,從而導致該塊內存永遠不會被再次使用
  • 資源泄漏:通常指的是系統資源,比如socket,文件描述符等,因為這些在系統中都是有限制的,如果創建了而不歸還,久而久之,就會耗盡資源,導致其他程序不可用

本文主要分析堆內存泄漏,所以后面的內存泄漏均指的是堆內存泄漏

根源

內存泄漏,主要指的是在堆(heap)上申請的動態內存泄漏,或者說是指針指向的內存塊忘了被釋放,導致該塊內存不能再被申請重新使用。

之前在知乎上看了一句話,指針是C的精髓,也是初學者的一個坎。換句話說,內存管理是C的精髓,C/C++可以直接跟OS打交道,從性能角度出發,開發者可以根據自己的實際使用場景靈活進行內存分配和釋放。

雖然在C++中自C++11引入了smart pointer,雖然很大程度上能夠避免使用裸指針,但仍然不能完全避免,最重要的一個原因是你不能保證組內其他人不適用指針,更不能保證合作部門不使用指針。

那么為什么C/C++中會存在指針呢?

這就得從進程的內存布局說起。

進程內存布局

ec2bffcc-e54c-11ec-ba43-dac502259ad0.png

上圖為32位進程的內存布局,從上圖中主要包含以下幾個塊:

  • 內核空間:供內核使用,存放的是內核代碼和數據
  • stack:這就是我們經常所說的棧,用來存儲自動變量(automatic variable)
  • mmap:也成為內存映射,用來在進程虛擬內存地址空間中分配地址空間,創建和物理內存的映射關系
  • heap:就是我們常說的堆,動態內存的分配都是在堆上
  • bss:包含所有未初始化的全局和靜態變量,此段中的所有變量都由0或者空指針初始化,程序加載器在加載程序時為BSS段分配內存
  • ds:初始化的數據塊
    • 包含顯式初始化的全局變量和靜態變量
    • 此段的大小由程序源代碼中值的大小決定,在運行時不會更改
    • 它具有讀寫權限,因此可以在運行時更改此段的變量值
    • 該段可進一步分為初始化只讀區和初始化讀寫區
  • text:也稱為文本段
    • 該段包含已編譯程序的二進制文件。
    • 該段是一個只讀段,用于防止程序被意外修改
    • 該段是可共享的,因此對于文本編輯器等頻繁執行的程序,內存中只需要一個副本

由于本文主要講內存分配相關,所以下面的內容僅涉及到棧(stack)和堆(heap)。

ec7b00ae-e54c-11ec-ba43-dac502259ad0.png

棧一塊連續的內存塊,棧上的內存分配就是在這一塊連續內存塊上進行操作的。編譯器在編譯的時候,就已經知道要分配的內存大小,當調用函數時候,其內部的遍歷都會在棧上分配內存;當結束函數調用時候,內部變量就會被釋放,進而將內存歸還給棧。

classObject{
public:
Object()=default;
//....
};

voidfun(){
Objectobj;

//dosth
}

在上述代碼中,obj就是在棧上進行分配,當出了fun作用域的時候,會自動調用Object的析構函數對其進行釋放。

前面有提到,局部變量會在作用域(如函數作用域、塊作用域等)結束后析構、釋放內存。因為分配和釋放的次序是剛好完全相反的,所以可用到堆棧先進后出(first-in-last-out, FILO)的特性,而 C++ 語言的實現一般也會使用到調用堆棧(call stack)來分配局部變量(但非標準的要求)。

因為棧上內存分配和釋放,是一個進棧和出棧的過程(對于編譯器只是一個指令),所以相比于堆上的內存分配,棧要快的多。

雖然棧的訪問速度要快于堆,每個線程都有一個自己的棧,棧上的對象是不能跨線程訪問的,這就決定了棧空間大小是有限制的,如果棧空間過大,那么在大型程序中幾十乃至上百個線程,光棧空間就消耗了RAM,這就導致heap的可用空間變小,影響程序正常運行。

設置

Linux系統上,可用通過如下命令來查看棧大小:

ulimit-s
10240

在筆者的機器上,執行上述命令輸出結果是10240(KB)即10m,可以通過shell命令修改棧大小。

ulimit-s102400

通過如上命令,可以將棧空間臨時修改為100m,可以通過下面的命令:

/etc/security/limits.conf

分配方式

靜態分配

靜態分配由編譯器完成,假如局部變量以及函數參數等,都在編譯期就分配好了。

voidfun(){
inta[10];
}

上述代碼中,a占10 * sizeof(int)個字節,在編譯的時候直接計算好了,運行的時候,直接進棧出棧。

動態分配

可能很多人認為只有堆上才會存在動態分配,在棧上只可能是靜態分配。其實,這個觀點是錯的,棧上也支持動態分配,該動態分配由alloca()函數進行分配。棧的動態分配和堆是不同的,通過alloca()函數分配的內存由編譯器進行釋放,無需手動操作。

特點

  • 分配速度快:分配大小由編譯器在編譯期完成
  • 不會產生內存碎片:棧內存分配是連續的,以FILO的方式進棧和出棧
  • 大小受限:棧的大小依賴于操作系統
  • 訪問受限:只能在當前函數或者作用域內進行訪問

堆(heap)是一種內存管理方式。內存管理對操作系統來說是一件非常復雜的事情,因為首先內存容量很大,其次就是內存需求在時間和大小塊上沒有規律(操作系統上運行著幾十甚至幾百個進程,這些進程可能隨時都會申請或者是釋放內存,并且申請和釋放的內存塊大小是隨意的)。

堆這種內存管理方式的特點就是自由(隨時申請、隨時釋放、大小塊隨意)。堆內存是操作系統劃歸給堆管理器(操作系統中的一段代碼,屬于操作系統的內存管理單元)來管理的,堆管理器提供了對應的接口_sbrk、_mmap等,只是該接口往往由運行時庫(Linux為glibc)進行調用,即也可以說由運行時庫進行堆內存管理,運行時庫提供了malloc/free函數由開發人員調用,進而使用堆內存。

分配方式

正如我們所理解的那樣,由于是在運行期進行內存分配,分配的大小也在運行期才會知道,所以堆只支持動態分配,內存申請和釋放的行為由開發者自行操作,這就很容易造成我們說的內存泄漏。

特點

  • 變量可以在進程范圍內訪問,即進程內的所有線程都可以訪問該變量
  • 沒有內存大小限制,這個其實是相對的,只是相對于棧大小來說沒有限制,其實最終還是受限于RAM
  • 相對棧來說訪問比較慢
  • 內存碎片
  • 由開發者管理內存,即內存的申請和釋放都由開發人員來操作

堆與棧區別

理解堆和棧的區別,對我們開發過程中會非常有用,結合上面的內容,總結下二者的區別。

對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產生memory leak

  • 空間大小不同
    • 一般來講在 32 位系統下,堆內存可以達到3G的空間,從這個角度來看堆內存幾乎是沒有什么限制的。
    • 對于棧來講,一般都是有一定的空間大小的,一般依賴于操作系統(也可以人工設置)
  • 能否產生碎片不同
    • 對于堆來講,頻繁的內存分配和釋放勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。
    • 對于棧來講,內存都是連續的,申請和釋放都是指令移動,類似于數據結構中的進棧和出棧
  • 增長方向不同
    • 對于堆來講,生長方向是向上的,也就是向著內存地址增加的方向
    • 對于棧來講,它的生長方向是向下的,是向著內存地址減小的方向增長
  • 分配方式不同
    • 堆都是動態分配的,比如我們常見的malloc/new;而棧則有靜態分配和動態分配兩種。
    • 靜態分配是編譯器完成的,比如局部變量的分配,而棧的動態分配則通過alloca()函數完成
    • 二者動態分配是不同的,棧的動態分配的內存由編譯器進行釋放,而堆上的動態分配的內存則必須由開發人自行釋放
  • 分配效率不同
    • 棧有操作系統分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高
    • 堆內存的申請和釋放專門有運行時庫提供的函數,里面涉及復雜的邏輯,申請和釋放效率低于棧

截止到這里,棧和堆的基本特性以及各自的優缺點、使用場景已經分析完成,在這里給開發者一個建議,能使用棧的時候,就盡量使用棧,一方面是因為效率高于堆,另一方面內存的申請和釋放由編譯器完成,這樣就避免了很多問題。

擴展

終于到了這一小節,其實,上面講的那么多,都是為這一小節做鋪墊。

在前面的內容中,我們對比了棧和堆,雖然棧效率比較高,且不存在內存泄漏、內存碎片等,但是由于其本身的局限性(不能多線程、大小受限),所以在很多時候,還是需要在堆上進行內存。

我們先看一段代碼:

#include
#include

intmain(){
inta;
int*p;
p=(int*)malloc(sizeof(int));
free(p);

return0;
}

上述代碼很簡單,有兩個變量a和p,類型分別為int和int *,其中,a和p存儲在棧上,p的值為在堆上的某塊地址(在上述代碼中,p的值為0x1c66010),上述代碼布局如下圖所示:

ec966650-e54c-11ec-ba43-dac502259ad0.png

產生方式

以產生的方式來分類,內存泄漏可以分為四類:

  • 常發性內存泄漏
  • 偶發性內存泄漏
  • 一次性內存泄漏
  • 隱式內存泄漏

常發性內存泄漏

產生內存泄漏的代碼或者函數會被多次執行到,在每次執行的時候,都會產生內存泄漏。

偶發性內存泄漏

常發性內存泄漏不同的是,偶發性內存泄漏函數只在特定的場景下才會被執行。

筆者在19年的時候,曾經遇到一個這種內存泄漏。有一個函數專門進行價格加密,每次泄漏3個字節,且只有在競價成功的時候,才會調用此函數進行價格加密,因此泄漏的非常不明顯。

當時發現這個問題,是上線后的第二天,幫忙排查線上問題,發現內存較上線前上漲了點(大概幾百兆的樣子),了解glibc內存分配原理的都清楚,調用delete后,內存不一定會歸還給OS,但是本著寧可信其有,不可信其無的心態,決定來分析是否真的存在內存泄漏。

當時用了個比較傻瓜式的方法,通過top命令,將該進程所占的內存輸出到本地文件,大概幾個小時后,將這些數據導入Excel中,內存占用基本呈一條斜線,所以基本能夠確定代碼存在內存泄漏,所以就對新上線的這部分代碼進行重新review,定位到泄漏點,然后修復,重新上線。

一次性內存泄漏

這種內存泄漏在程序的生命周期內只會泄漏一次,或者說造成泄漏的代碼只會被執行一次。

有的時候,這種可能不算內存泄漏,或者說設計如此。就以筆者現在線上的服務來說,類似于如下這種:

intmain(){
auto*service=newService;
//dosth
service->Run();//服務啟動
service->Loop();//可以理解為一個sleep,目的是使得程序不退出
return0;
}

這種嚴格意義上,并不算內存泄漏,因為程序是這么設計的,即使程序異常退出,那么整個服務進程也就退出了,當然,在Loop()后面加個delete更好。

隱式內存泄漏

程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這里并沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對于一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。

比較常見的隱式內存泄漏有以下三種:

  • 內存碎片:還記得我們之前的那篇文章深入理解glibc內存管理精髓,程序跑了幾天之后,進程就因為OOM導致了退出,就是因為內存碎片導致剩下的內存不能被重新分配導致
  • 即使我們調用了free/delete,運行時庫不一定會將內存歸還OS,具體深入理解glibc內存管理精髓
  • 用過STL的知道,STL內部有一個自己的allocator,我們可以當做一個memory poll,當調用vector.clear()時候,內存并不會歸還OS,而是放回allocator,其內部根據一定的策略,在特定的時候將內存歸還OS,是不是跟glibc原理很像

分類

未釋放

這種是很常見的,比如下面的代碼:

intfun(){
char*pBuffer=malloc(sizeof(char));

/*Dosomework*/
return0;
}

上面代碼是非常常見的內存泄漏場景(也可以使用new來進行分配),我們申請了一塊內存,但是在fun函數結束時候沒有調用free函數進行內存釋放。

在C++開發中,還有一種內存泄漏,如下:

classObj{
public:
Obj(intsize){
buffer_=newchar;
}
~Obj(){}
private:
char*buffer_;
};

intfun(){
Objectobj;
//dosth
return0;
}

上面這段代碼中,析構函數沒有釋放成員變量buffer_指向的內存,所以在編寫析構函數的時候,一定要仔細分析成員變量有沒有申請動態內存,如果有,則需要手動釋放,我們重新編寫了析構函數,如下:

~Object(){
deletebuffer_;
}

在C/C++中,對于普通函數,如果申請了堆資源,請跟進代碼的具體場景調用free/delete進行資源釋放;對于class,如果申請了堆資源,則需要在對應的析構函數中調用free/delete進行資源釋放。

未匹配

在C++中,我們經常使用new操作符來進行內存分配,其內部主要做了兩件事:

  1. 通過operator new從堆上申請內存(glibc下,operator new底層調用的是malloc)
  2. 調用構造函數(如果操作對象是一個class的話)

對應的,使用delete操作符來釋放內存,其順序正好與new相反:

  1. 調用對象的析構函數(如果操作對象是一個class的話)
  2. 通過operator delete釋放內存
void*operatornew(std::size_tsize){
void*p=malloc(size);
if(p==nullptr){
throw("newfailedtoallocate%zubytes",size);
}
returnp;
}
void*operatornew[](std::size_tsize){
void*p=malloc(size);
if(p==nullptr){
throw("new[]failedtoallocate%zubytes",size);
}
returnp;
}

voidoperatordelete(void*ptr)throw(){
free(ptr);
}
voidoperatordelete[](void*ptr)throw(){
free(ptr);
}

為了加深多這塊的理解,我們舉個例子:

classTest{
public:
Test(){
std::cout<"inTest"<std::endl;
}
//other
~Test(){
std::cout<"in~Test"<std::endl;
}
};

intmain(){
Test*t=newTest;
//dosth
deletet;
return0;
}

在上述main函數中,我們使用new 操作符創建一個Test類指針

  1. 通過operator new申請內存(底層malloc實現)
  2. 通過placement new在上述申請的內存塊上調用構造函數
  3. 調用ptr->~Test()釋放Test對象的成員變量
  4. 調用operator delete釋放內存

上述過程,可以理解為如下:

//new
void*ptr=malloc(sizeof(Test));
t=new(ptr)Test

//delete
ptr->~Test();
free(ptr);

好了,上述內容,我們簡單的講解了C++中new和delete操作符的基本實現以及邏輯,那么,我們就簡單總結下下產生內存泄漏的幾種類型。

new 和 free

仍然以上面的Test對象為例,代碼如下:

Test*t=newTest;
free(t)

此處會產生內存泄漏,在上面,我們已經分析過,new操作符會先通過operator new分配一塊內存,然后在該塊內存上調用placement new即調用Test的構造函數。而在上述代碼中,只是通過free函數釋放了內存,但是沒有調用Test的析構函數以釋放Test的成員變量,從而引起內存泄漏

new[] 和 delete

intmain(){
Test*t=newTest[10];
//dosth
deletet;
return0;
}

在上述代碼中,我們通過new創建了一個Test類型的數組,然后通delete操作符刪除該數組,編譯并執行,輸出如下:

inTest
inTest
inTest
inTest
inTest
inTest
inTest
inTest
inTest
inTest
in~Test

從上面輸出結果可以看出,調用了10次構造函數,但是只調用了一次析構函數,所以引起了內存泄漏。這是因為調用delete t釋放了通過operator new[]申請的內存,即malloc申請的內存塊,且只調用了t[0]對象的析構函數,t[1..9]對象的析構函數并沒有被調用。

虛析構

記得08年面谷歌的時候,有一道題,面試官問,std::string能否被繼承,為什么?

當時沒回答上來,后來過了沒多久,進行面試復盤的時候,偶然看到繼承需要父類析構函數為virtual,才恍然大悟,原來考察點在這塊。

下面我們看下std::string的析構函數定義:

~basic_string(){
_M_rep()->_M_dispose(this->get_allocator());
}

這塊需要特別說明下,std::basic_string是一個模板,而std::string是該模板的一個特化,即std::basic_string。

typedefstd::basic_string<char>string;

現在我們可以給出這個問題的答案:不能,因為std::string的析構函數不為virtual,這樣會引起內存泄漏

仍然以一個例子來進行證明。

classBase{
public:
Base(){
buffer_=newchar[10];
}

~Base(){
std::cout<"inBase::~Base"<std::endl;
delete[]buffer_;
}
private:
char*buffer_;

};

classDerived:publicBase{
public:
Derived(){}

~Derived(){
std::cout<"intDerived::~Derived"<std::endl;
}
};

intmain(){
Base*base=newDerived;
deletebase;
return0;
}

上面代碼輸出如下:

inBase::~Base

可見,上述代碼并沒有調用派生類Derived的析構函數,如果派生類中在堆上申請了資源,那么就會產生內存泄漏

為了避免因為繼承導致的內存泄漏,我們需要將父類的析構函數聲明為virtual,代碼如下(只列了部分修改代碼,其他不變):

~Base(){
std::cout<"inBase::~Base"<std::endl;
delete[]buffer_;
}

然后重新執行代碼,輸出結果如下:

intDerived::~Derived
inBase::~Base

借助此文,我們再次總結下存在繼承情況下,構造函數和析構函數的調用順序。

派生類對象在創建時構造函數調用順序:

  1. 調用父類的構造函數
  2. 調用父類成員變量的構造函數
  3. 調用派生類本身的構造函數

派生類對象在析構時的析構函數調用順序:

  1. 執行派生類自身的析構函數
  2. 執行派生類成員變量的析構函數
  3. 執行父類的析構函數

為了避免存在繼承關系時候的內存泄漏,請遵守一條規則:無論派生類有沒有申請堆上的資源,請將父類的析構函數聲明為virtual

循環引用

在C++開發中,為了盡可能的避免內存泄漏,自C++11起引入了smart pointer,常見的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已經被廢棄),其中weak_ptr是為了解決循環引用而存在,其往往與shared_ptr結合使用。

下面,我們看一段代碼:

classController{
public:
Controller()=default;

~Controller(){
std::cout<"in~Controller"<std::endl;
}

classSubController{
public:
SubController()=default;

~SubController(){
std::cout<"in~SubController"<std::endl;
}

std::shared_ptrcontroller_;
};

std::shared_ptrsub_controller_;
};

intmain(){
autocontroller=std::make_shared();
autosub_controller=std::make_shared();

controller->sub_controller_=sub_controller;
sub_controller->controller_=controller;
return0;
}

編譯并執行上述代碼,發現并沒有調用Controller和SubController的析構函數,我們嘗試著打印下引用計數,代碼如下:

intmain(){
autocontroller=std::make_shared();
autosub_controller=std::make_shared();

controller->sub_controller_=sub_controller;
sub_controller->controller_=controller;

std::cout<"controlleruse_count:"<std::endl;
std::cout<"sub_controlleruse_count:"<std::endl;
return0;
}

編譯并執行之后,輸出如下:

controlleruse_count:2
sub_controlleruse_count:2

通過上面輸出可以發現,因為引用計數都是2,所以在main函數結束的時候,不會調用controller和sub_controller的析構函數,所以就出現了內存泄漏

上面產生內存泄漏的原因,就是我們常說的循環引用

ecb55b96-e54c-11ec-ba43-dac502259ad0.png

為了解決std::shared_ptr循環引用導致的內存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環。

classController{
public:
Controller()=default;

~Controller(){
std::cout<"in~Controller"<std::endl;
}

classSubController{
public:
SubController()=default;

~SubController(){
std::cout<"in~SubController"<std::endl;
}

std::weak_ptrcontroller_;
};

std::shared_ptrsub_controller_;
};

在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr,重新編譯執行,結果如下:

controlleruse_count:1
sub_controlleruse_count:2
in~Controller
in~SubController

從上面結果可以看出,controller和sub_controller均以釋放,所以循環引用引起的內存泄漏問題,也得以解決。

ecdb96d0-e54c-11ec-ba43-dac502259ad0.png

可能有人會問,使用std::shared_ptr可以直接訪問對應的成員函數,如果是std::weak_ptr的話,怎么訪問呢?我們可以使用下面的方式:

std::shared_ptrcontroller=controller_.lock();

即在子類SubController中,如果要使用controller調用其對應的函數,就可以使用上面的方式。

避免

避免在堆上分配

眾所周知,大部分的內存泄漏都是因為在堆上分配引起的,如果我們不在堆上進行分配,就不會存在內存泄漏了(這不廢話嘛),我們可以根據具體的使用場景,如果對象可以在棧上進行分配,就在棧上進行分配,一方面棧的效率遠高于堆,另一方面,還能避免內存泄漏,我們何樂而不為呢。

手動釋放

  • 對于malloc函數分配的內存,在結束使用的時候,使用free函數進行釋放
  • 對于new操作符創建的對象,切記使用delete來進行釋放
  • 對于new []創建的對象,使用delete[]來進行釋放(使用free或者delete均會造成內存泄漏)

避免使用裸指針

盡可能避免使用裸指針,除非所調用的lib庫或者合作部門的接口是裸指針。

intfun(int*ptr){//fun是一個接口或lib函數
//dosth

return0;
}

intmain(){}
inta=1000;
int*ptr=&a;
//...
fun(ptr);

return0;
}

在上面的fun函數中,有一個參數ptr,為int *,我們需要根據上下文來分析這個指針是否需要釋放,這是一種很不好的設計

使用STL中或者自己實現對象

在C++中,提供了相對完善且可靠的STL供我們使用,所以能用STL的盡可能的避免使用C中的編程方式,比如:

  • 使用std::string 替代char *, string類自己會進行內存管理,而且優化的相當不錯
  • 使用std::vector或者std::array來替代傳統的數組
  • 其它適合使用場景的對象

智能指針

自C++11開始,STL中引入了智能指針(smart pointer)來動態管理資源,針對使用場景的不同,提供了以下三種智能指針。

unique_ptr

unique_ptr是限制最嚴格的一種智能指針,用來替代之前的auto_ptr,獨享被管理對象指針所有權。當unique_ptr對象被銷毀時,會在其析構函數內刪除關聯的原始指針。

unique_ptr對象分為以下兩類:

  • unique_ptr該類型的對象關聯了單個Type類型的指針

    std::unique_ptrp1(newType);//c++11
    autop1=std::make_unique();//c++14
    
  • unique_ptr 該類型的對象關聯了多個Type類型指針,即一個對象數組

    std::unique_ptrp2(newType[n]());//c++11
    autop2=std::make_unique(n);//c++14
    
  • 不可用被復制

    unique_ptr<int>a(newint(0));
    unique_ptr<int>b=a;//編譯錯誤
    unique_ptr<int>b=std::move(a);//可以通過move語義進行所有權轉移
    

根據使用場景,可以使用std::unique_ptr來避免內存泄漏,如下:

voidfun(){
unique_ptr<int>a(newint(0));
//usea
}

在上述fun函數結束的時候,會自動調用a的析構函數,從而釋放其關聯的指針。

shared_ptr

與unique_ptr不同的是,unique_ptr是獨占管理權,而shared_ptr則是共享管理權,即多個shared_ptr可以共用同一塊關聯對象,其內部采用的是引用計數,在拷貝的時候,引用計數+1,而在某個對象退出作用域或者釋放的時候,引用計數-1,當引用計數為0的時候,會自動釋放其管理的對象。

voidfun(){
std::shared_ptra;//a是一個空對象
{
std::shared_ptrb=std::make_shared();//分配資源
a=b;//此時引用計數為2
{
std::shared_ptrc=a;//此時引用計數為3
}//c退出作用域,此時引用計數為2
}//b退出作用域,此時引用計數為1
}//a退出作用域,引用計數為0,釋放對象

weak_ptr

weak_ptr的出現,主要是為了解決shared_ptr的循環引用,其主要是與shared_ptr一起來私用。和shared_ptr不同的地方在于,其并不會擁有資源,也就是說不能訪問對象所提供的成員函數,不過,可以通過weak_ptr.lock()來產生一個擁有訪問權限的shared_ptr。

std::weak_ptra;
{
std::shared_ptrb=std::make_shared();
a=b
}//b所對應的資源釋放

RAII

RAIIResource Acquisition is Initialization(資源獲取即初始化)的縮寫,是C++語言的一種管理資源,避免泄漏的用法。

利用的就是C++構造的對象最終會被銷毀的原則。利用C++對象生命周期的概念來控制程序的資源,比如內存,文件句柄,網絡連接等。

RAII的做法是使用一個對象,在其構造時獲取對應的資源,在對象生命周期內控制對資源的訪問,使之始終保持有效,最后在對象析構的時候,釋放構造時獲取的資源。

簡單地說,就是把資源的使用限制在對象的生命周期之中,自動釋放。

舉個簡單的例子,通常在多線程編程的時候,都會用到std::mutex,如下代碼:

std::mutexmutex_;

voidfun(){
mutex_.lock();

if(...){
mutex_.unlock();
return;
}

mutex_.unlock()
}

在上述代碼中,如果if分支多的話,每個if分支里面都要釋放鎖,如果一不小心忘記釋放,那么就會造成故障,為了解決這個問題,我們使用RAII技術,代碼如下:

std::mutexmutex_;

voidfun(){
std::lock_guard<std::mutex>guard(mutex_);

if(...){
return;
}
}

在guard出了fun作用域的時候,會自動調用mutex_.lock()進行釋放,避免了很多不必要的問題。

定位

在發現程序存在內存泄漏后,往往需要定位泄漏點,而定位這一步往往是最困難的,所以經常為了定位泄漏點,采取各種各樣的方案,甭管方案優雅與否,畢竟管他白貓黑貓,抓住老鼠才是好貓,所以在本節,簡單說下筆者這么多年定位泄漏點的方案,有些比較邪門歪道,您就隨便看看就行。

日志

這種方案的核心思想,就是在每次分配內存的時候,打印指針地址,在釋放內存的時候,打印內存地址,這樣在程序結束的時候,通過分配和釋放的差,如果分配的條數大于釋放的條數,那么基本就能確定程序存在內存泄漏,然后根據日志進行詳細分析和定位。

char*fun(){
char*p=(char*)malloc(20);
printf("%s,%d,addressis:%p",__FILE__,__LINE__,p);
//dosth
returnp;
}

intmain(){
fun();

return0;
}

統計

統計方案可以理解為日志方案的一種特殊實現,其主要原理是在分配的時候,統計分配次數,在釋放的時候,則是統計釋放的次數,這樣在程序結束前判斷這倆值是否一致,就能判斷出是否存在內存泄漏。

此方法可幫助跟蹤已分配內存的狀態。為了實現這個方案,需要創建三個自定義函數,一個用于內存分配,第二個用于內存釋放,最后一個用于檢查內存泄漏。代碼如下:

staticunsignedintallocated=0;
staticunsignedintdeallocated=0;
void*Memory_Allocate(size_tsize)
{
void*ptr=NULL;
ptr=malloc(size);
if(NULL!=ptr){
++allocated;
}else{
//Logerror
}
returnptr;
}
voidMemory_Deallocate(void*ptr){
if(pvHandle!=NULL){
free(ptr);
++deallocated;
}
}
intCheck_Memory_Leak(void){
intret=0;
if(allocated!=deallocated){
//Logerror
ret=MEMORY_LEAK;
}else{
ret=OK;
}
returnret;
}

工具

在Linux上比較常用的內存泄漏檢測工具是valgrind,所以咱們就以valgrind為工具,進行檢測。

我們首先看一段代碼:

#include

voidfunc(void){
char*buff=(char*)malloc(10);
}

intmain(void){
func();//產生內存泄漏
return0;
}
  • 通過gcc -g leak.c -o leak命令進行編譯
  • 執行valgrind --leak-check=full ./leak

在上述的命令執行后,會輸出如下:

==9652==Memcheck,amemoryerrordetector
==9652==Copyright(C)2002-2017,andGNUGPL'd,byJulianSewardetal.
==9652==UsingValgrind-3.15.0andLibVEX;rerunwith-hforcopyrightinfo
==9652==Command:./leak
==9652==
==9652==
==9652==HEAPSUMMARY:
==9652==inuseatexit:10bytesin1blocks
==9652==totalheapusage:1allocs,0frees,10bytesallocated
==9652==
==9652==10bytesin1blocksaredefinitelylostinlossrecord1of1
==9652==at0x4C29F73:malloc(vg_replace_malloc.c:309)
==9652==by0x40052E:func(leak.c:4)
==9652==by0x40053D:main(leak.c:8)
==9652==
==9652==LEAKSUMMARY:
==9652==definitelylost:10bytesin1blocks
==9652==indirectlylost:0bytesin0blocks
==9652==possiblylost:0bytesin0blocks
==9652==stillreachable:0bytesin0blocks
==9652==suppressed:0bytesin0blocks
==9652==
==9652==Forlistsofdetectedandsuppressederrors,rerunwith:-s
==9652==ERRORSUMMARY:1errorsfrom1contexts(suppressed:0from0)

valgrind的檢測信息將內存泄漏分為如下幾類:

  • definitely lost:確定產生內存泄漏
  • indirectly lost:間接產生內存泄漏
  • possibly lost:可能存在內存泄漏
  • still reachable:即使在程序結束時候,仍然有指針在指向該塊內存,常見于全局變量

主要上面輸出的下面幾句:

==9652==by0x40052E:func(leak.c:4)
==9652==by0x40053D:main(leak.c:8)

提示在main函數(leak.c的第8行)fun函數(leak.c的第四行)產生了內存泄漏,通過分析代碼,原因定位,問題解決。

valgrind不僅可以檢測內存泄漏,還有其他很強大的功能,由于本文以內存泄漏為主,所以其他的功能就不在此贅述了,有興趣的可以通過valgrind --help來進行查看

?

對于Windows下的內存泄漏檢測工具,筆者推薦一款輕量級功能卻非常強大的工具UMDH,筆者在十二年前,曾經在某外企負責內存泄漏,代碼量幾百萬行,光編譯就需要兩個小時,嘗試了各種工具(免費的和收費的),最終發現了UMDH,如果你在Windows上進行開發,強烈推薦。

?

經驗之談

在C/C++開發過程中,內存泄漏是一個非常常見的問題,其影響相對來說遠低于coredump等,所以遇到內存泄漏的時候,不用過于著急,大不了重啟嘛。

在開發過程中遵守下面的規則,基本能90+%避免內存泄漏:

  • 良好的編程習慣,只有有malloc/new,就得有free/delete
  • 盡可能的使用智能指針,智能指針就是為了解決內存泄漏而產生
  • 使用log進行記錄
  • 也是最重要的一點,誰申請,誰釋放

對于malloc分配內存,分配失敗的時候返回值為NULL,此時程序可以直接退出了,而對于new進行內存分配,其分配失敗的時候,是拋出std::bad_alloc,所以為了第一時間發現問題,不要對new異常進行catch,畢竟內存都分配失敗了,程序也沒有運行的必要了。

如果我們上線后,發現程序存在內存泄漏,如果不嚴重的話,可以先暫時不管線上,同時進行排查定位;如果線上泄漏比較嚴重,那么第一時間根據實際情況來決定是否回滾。在定位問題點的時候,可以采用縮小范圍法,著重分析這次新增的代碼,這樣能夠有效縮短問題解決的時間。

結語

C/C++之所以復雜、效率高,是因為其靈活性,可用直接訪問操作系統API,而正因為其靈活性,就很容易出問題,團隊成員必須愿意按照一定的規則來進行開發,有完整的review機制,將問題暴露在上線之前。

這樣才可以把經歷放在業務本身,而不是查找這些問題上,有時候往往一個小問題就能消耗很久的時間去定位解決,所以,一定要有一個良好的開發習慣

審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 內存
    +關注

    關注

    9

    文章

    3210

    瀏覽量

    76375
  • C++
    C++
    +關注

    關注

    22

    文章

    2124

    瀏覽量

    77117

原文標題:內存泄漏-原因、避免以及定位

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    curl中的TFTP實現:整數下溢導致內存越界讀取漏洞

    漏洞概述 在 curl 的 TFTP 協議實現中發現了一個漏洞,該漏洞可能導致 curl 或使用 libcurl 的應用程序在特定條件下,向惡意的 TFTP 服務器發送超出已分配內存塊邊界的內存數據
    發表于 02-19 13:55

    硬件問題造成的MCU死機的原因

    關于MCU死機問題,近期小編在出差期間遇到多起,且原因不同。所以,今日小白借此機會講一講因硬件問題造成的MCU死機。 MCU不良 在遇到死機問題時,已經可以判定是硬件原因造成的前提下
    發表于 11-24 08:07

    WebGL/Canvas 內存泄露分析

    在構建高性能、長周期運行的 WebGL/Canvas 應用(如 3D 編輯器、數據可視化平臺)時,內存管理是一個至關重要且極具挑戰性的課題。 開發者通常面臨的內存泄漏問題,其根源遠比簡單
    的頭像 發表于 10-21 11:40 ?409次閱讀
    WebGL/Canvas <b class='flag-5'>內存</b>泄露分析

    國巨電容出現漏液現象,可能是哪些原因導致的?

    國巨電容出現漏液現象,可能是由密封結構失效、電化學腐蝕、機械損傷、材料老化、環境應力以及制造缺陷等多種因素導致的,以下是對這些原因的詳細分析: 密封結構失效 焊接不良 :國巨電容的金屬外殼與密封蓋
    的頭像 發表于 09-29 14:21 ?620次閱讀
    國巨電容出現漏液現象,可能是哪些<b class='flag-5'>原因</b><b class='flag-5'>導致</b>的?

    at_device 包 ml307長時間運行有內存泄漏問題怎么解決?

    使用 at_device 包中的 ml307 包長時間運行有大量內存泄漏問題,大概漲了20K,求助解決。
    發表于 09-24 07:41

    使用lv_label_set_text釋放內存沒對齊是什么原因導致的?

    (guider_ui.monitor_label_pressure_now, "1"); rt_mutex_release(lv_mutex); // 釋放互斥鎖 使用lv_label_set_text導致釋放內存沒對齊是什么問題 已經加了互斥鎖
    發表于 09-16 06:44

    產品密封性防水泄漏標準如何判定?精誠工科氣密性檢測儀的設置方法

    工程師們經常遇到的實際問題。下面,精誠工科結合行業經驗,為您詳細解讀。一、正確認識產品泄漏泄漏的本質是什么?泄漏不是簡單的「漏」與「不漏」,而是分子級別的傳質過程
    的頭像 發表于 09-15 14:02 ?966次閱讀
    產品密封性防水<b class='flag-5'>泄漏</b>標準如何判定?精誠工科氣密性檢測儀的設置方法

    在線程刪除時遇到斷言,是什么原因導致的?

    在一個線程中調用線程刪除函數刪除另外一個線程,這2個線程的優先級是相等的,被刪除的線程也是動態創建的,出現了下面的斷言內容,一般是什么情況導致的?堆棧分配不足?刪除的線程和被刪除的線程哪個堆棧分配
    發表于 09-12 06:08

    線程超時函數中 assert 失敗是什么原因導致的?

    最近調試 gd32h759 遇到了一個十分奇怪的問題,在初步調通所有的邏輯功能后,發現系統經常會在運行一段時間后死在一個奇怪的線程超時函數中 assert 失敗導致卡死。用 cmbacktrace
    發表于 09-09 06:56

    你知道什么原因導致安規電容損壞嗎?

    安規電容通常用于抑制噪聲、濾波或電氣隔離等。安規電容在設計時必須具備一定的安全標準,以保證在故障情況下不會對使用者造成電擊或火災等危險。然而,安規電容也有可能因各種原因發生損壞,常見的原因包括: 一
    的頭像 發表于 07-13 11:03 ?1311次閱讀

    在OpenVINO? C++代碼中啟用 AddressSanitizer 時的內存泄漏怎么解決?

    在 OpenVINO? C++代碼中啟用 AddressSanitizer 時遇到內存泄漏: \"#0 0xaaaab8558370 in operator new(unsigned
    發表于 06-23 07:16

    網絡光纖出問題一般是什么原因導致的呢

    (如道路開挖)、重物碾壓、動物啃咬(如老鼠咬斷)。 案例:某小區寬帶中斷,經排查發現施工隊挖斷主干光纖,導致整棟樓斷網。 檢測方法:使用OTDR(光時域反射儀)定位斷點位置。 光纖彎曲過度 原因:布線時彎曲半徑過小( 案例:辦公室網絡時斷
    的頭像 發表于 06-17 10:05 ?3955次閱讀
    網絡光纖出問題一般是什么<b class='flag-5'>原因</b><b class='flag-5'>導致</b>的呢

    HarmonyOS優化應用內存占用問題性能優化一

    應用開發中,可以使用虛引用(Weak Reference)來避免內存泄漏。通過使用Weak Reference,可以避免循環引用導致內存泄漏
    發表于 05-21 11:27

    快速搞懂C語言程序內存分區!

    到動態分配的數據等內容。(內存分區圖示)理解這些內存分區的結構和特性,不僅有助于編寫更高效的代碼,還能幫助排查和解決如段錯誤、內存泄漏、棧溢出等常見問題。以下是常見的六
    的頭像 發表于 03-14 17:37 ?1571次閱讀
    快速搞懂C語言程序<b class='flag-5'>內存</b>分區!

    stm32F407平臺上使用freertos,使用pvPortMalloc申請內存,發現內存中的數據總被修改,怎么解決?

    如題,我現在在stm32F407平臺上打開FREERTOS,然后使用pvPortMalloc動態申請內存的時候,發現這塊內存中的數據總是變化,后面改為malloc申請,內存中的數據就符合預期了,我已經按照網上的流程配置了free
    發表于 03-07 09:03