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

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

3天內(nèi)不再提示

如何避免內(nèi)存泄漏的方法和原則

Q4MP_gh_c472c21 ? 來源:搜狐網(wǎng) ? 作者:搜狐網(wǎng) ? 2020-10-21 14:30 ? 次閱讀

本文向讀者介紹了如何避免內(nèi)存泄漏的方法和原則,在細節(jié)和大體方向上均給出一些可行性方案。讀者可以嘗試文中提出的方法,改進自己的代碼,大大減少內(nèi)存泄漏的可能性。

前言

近年來,討論 C++ 的人越來越少了,一方面是由于像 Python,Go 等優(yōu)秀的語言的流行,另一方面,大家也越來越明白一個道理,并不是所有的場景都必須使用 C++ 進行開發(fā)。Python 可以應(yīng)付大部分對性能要求不高的場景,Go 可以應(yīng)付大部分對并發(fā)要求較高的場景,而由于 C++ 的復(fù)雜性,只有在對性能極其苛刻的場景下,才會考慮使用。

那么到底多苛刻算是苛刻呢?Go 自帶內(nèi)存管理,也就是 GC 功能,經(jīng)過多年的優(yōu)化,在 Go 中每次 GC 可能會引入 500us 的 STW 延遲。


也就是說,如果你的應(yīng)用場景可以容忍不定期的 500us 的延遲,那么用 Go 都是沒有問題的。如果你無法容忍 500us 的延遲,那么帶 GC 功能的語言就基本無法使用了,只能選擇自己管理內(nèi)存的語言,例如 C++。那么由手動管理內(nèi)存而帶來的編程復(fù)雜度也就隨之而來了。
作為 C++ 程序員,內(nèi)存泄露始終是懸在頭上的一顆炸彈。在過去幾年的 C++ 開發(fā)過程中,由于我們采用了一些技術(shù),我們的程序發(fā)生內(nèi)存泄露的情況屈指可數(shù)。今天就在這里向大家做一個簡單的介紹。

內(nèi)存是如何泄露的

在 C++ 程序中,主要涉及到的內(nèi)存就是『?!缓汀憾选唬ㄆ渌糠植辉诒疚闹薪榻B了)。

通常來說,一個線程的棧內(nèi)存是有限的,通常來說是 8M 左右(取決于運行的環(huán)境)。棧上的內(nèi)存通常是由編譯器來自動管理的。當在棧上分配一個新的變量時,或進入一個函數(shù)時,棧的指針會下移,相當于在棧上分配了一塊內(nèi)存。我們把一個變量分配在棧上,也就是利用了棧上的內(nèi)存空間。當這個變量的生命周期結(jié)束時,棧的指針會上移,相同于回收了內(nèi)存。

由于棧上的內(nèi)存的分配和回收都是由編譯器控制的,所以在棧上是不會發(fā)生內(nèi)存泄露的,只會發(fā)生棧溢出(Stack Overflow),也就是分配的空間超過了規(guī)定的棧大小。

而堆上的內(nèi)存是由程序直接控制的,程序可以通過 malloc/free 或 new/delete 來分配和回收內(nèi)存,如果程序中通過 malloc/new 分配了一塊內(nèi)存,但忘記使用 free/delete 來回收內(nèi)存,就發(fā)生了內(nèi)存泄露。

經(jīng)驗 #1:盡量避免在堆上分配內(nèi)存

既然只有堆上會發(fā)生內(nèi)存泄露,那第一原則肯定是避免在堆上面進行內(nèi)存分配,盡可能的使用棧上的內(nèi)存,由編譯器進行分配和回收,這樣當然就不會有內(nèi)存泄露了。

然而,只在棧上分配內(nèi)存,在有 IO 的情況下是存在一定局限性的。 舉個例子,為了完成一個請求,我們通常會為這個請求構(gòu)造一個 Context 對象,用于描述和這個請求有關(guān)的一些上下文。例如下面一段代碼:

void Foo(Reuqest* req) { RequestContext ctx(req); HandleRequest(&ctx);}

如果 HandleRequest 是一個同步函數(shù),當這個函數(shù)返回時,請求就可以被處理完成,那么顯然 ctx 是可以被分配在棧上的。

但如果 HandleRequest 是一個異步函數(shù),例如:

void HandleRequest(RequestContext* ctx, Callback cb);

那么顯然,ctx 是不能被分配在棧上的,因為如果 ctx 被分配在棧上,那么當 Foo 函數(shù)推出后,ctx 對象的生命周期也就結(jié)束了。而 FooCB 中顯然會使用到 ctx 對象。

void HandleRequest(RequestContext* ctx, Callback cb); void Foo(Reuqest* req) { auto ctx = new RequestContext(req); HandleRequest(ctx, FooCB);} void FooCB(RequestContext* ctx) { FinishRequest(ctx); delete ctx;} 在這種情況下,如果忘記在 FooCB 中調(diào)用 delete ctx,則就會觸發(fā)內(nèi)存泄露。盡管我們可以借助一些靜態(tài)檢查工具對代碼進行檢查,但往往異步程序的邏輯是極其復(fù)雜的,一個請求的生命周期中,也需要進行大量的內(nèi)存分配操作,靜態(tài)檢查工具往往無法發(fā)現(xiàn)所有的內(nèi)存泄露情況。

那么怎么才能避免這種情況的產(chǎn)生呢?引入智能指針顯然是一種可行的方法,但引入 shared_ptr 往往引入了額外的性能開銷,并不十分理想。 在 SmartX,我們通常采用兩種方法來應(yīng)對這種情況。

經(jīng)驗 #2:使用 Arena

Arena 是一種統(tǒng)一化管理內(nèi)存生命周期的方法。所有需要在堆上分配的內(nèi)存,不通過 malloc/new,而是通過 Arena 的 CreateObject 接口。同時,不需要手動的執(zhí)行 free/delete,而是在 Arena 被銷毀的時候,統(tǒng)一釋放所有通過 Arena 對象申請的內(nèi)存。所以,只需要確保 Arena 對象一定被銷毀就可以了,而不用再關(guān)心其他對象是否有漏掉的 free/delete。這樣顯然降低了內(nèi)存管理的復(fù)雜度。

此外,我們還可以將 Arena 的生命周期與 Request 的生命周期綁定,一個 Request 生命周期內(nèi)的所有內(nèi)存分配都通過 Arena 完成。這樣的好處是,我們可以在構(gòu)造 Arena 的時候,大概預(yù)估出處理完成這個 Request 會消耗多少內(nèi)存,并提前將會使用到的內(nèi)存一次性的申請完成,從而減少了在處理一個請求的過程中,分配和回收內(nèi)存的次數(shù),從而優(yōu)化了性能。

我們最早看到 Arena 的思想,是在 LevelDB 的代碼中。這段代碼相當簡單,建議大家直接閱讀。

經(jīng)驗 #3:使用 Coroutine

Coroutine 相信大家并不陌生,那 Coroutine 的本質(zhì)是什么?我認為 Coroutine 的本質(zhì),是使得一個線程中可以存在多個上下文,并可以由用戶控制在多個上下文之間進行切換。而在上下文中,一個重要的組成部分,就是棧指針。使用 Coroutine,意味著我們在一個線程中,可以創(chuàng)造(或模擬)多個棧。

有了多個棧,意味著當我們要做一個異步處理時,不需要釋放當前棧上的內(nèi)存,而只需要切換到另一個棧上,就可以繼續(xù)做其他的事情了,當異步處理完成時,可以再切換回到這個棧上,將這個請求處理完成。 還是以剛才的代碼為示例:

void Foo(Reuqest* req) { RequestContext ctx(req); HandleRequest(&ctx);} void HandleRequest(RequestCtx* ctx) { SubmitAsync(ctx); Coroutine::Self()-》Yield(); CompleteRequest(ctx);} 這里的精髓在于,盡管 Coroutine::Self()-》Yield() 被調(diào)用時,程序可以跳出 HandleRequest 函數(shù)去執(zhí)行其他代碼邏輯,但當前的棧卻被保存了下來,所以 ctx 對象是安全的,并沒有被釋放。

這樣一來,我們就可以完全拋棄在堆上申請內(nèi)存,只是用棧上的內(nèi)存,就可以完成請求的處理,完全不用考慮內(nèi)存泄露的問題。然而這種假設(shè)過于理想,由于在棧上申請內(nèi)存存在一定的限制,例如棧大小的限制,以及需要在編譯是知道分配內(nèi)存的大小,所以在實際場景中,我們通常會結(jié)合使用 Arena 和 Coroutine 兩種技術(shù)一起使用。

有人可能會提到,想要多個棧用多個線程不就可以了?然而用多線程實現(xiàn)多個棧的問題在于,線程的創(chuàng)建和銷毀的開銷極大,且線程間切塊,也就是在棧之間進行切換的代銷需要經(jīng)過操作系統(tǒng),這個開銷也是極大的。所以想用線程模擬多個棧的想法在實際場景中是走不通的。

關(guān)于 Coroutine 有很多開源的實現(xiàn)方式,大家可以在 github 上找到很多,C++20 標準也會包含 Coroutine 的支持。在 SmartX 內(nèi)部,我們很早就實現(xiàn)了 Coroutine,并對所有異步 IO 操作進行了封裝,示例可參考我們之前的一篇文章 smartx:基于 Coroutine 的異步 RPC 框架示例(C++)

這里需要強調(diào)一下,Coroutine 確實會帶來一定的性能開銷,通常 Coroutine 切換的開銷在 20ns 以內(nèi),然而我們依然在對性能要求很苛刻的場景使用 Coroutine,一方面是因為 20ns 的性能開銷是相對很小的,另一方面是因為 Coroutine 極大的降低了異步編程的復(fù)雜度,降低了內(nèi)存泄露的可能性,使得編寫異步程序像編寫同步程序一樣簡單,降低了程序員心智的開銷。

boost 實現(xiàn)的 coroutine 的性能測試結(jié)果

經(jīng)驗 #4:善用 RAII

盡管在有些場景使用了 Coroutine,但還是可能會有在堆上申請內(nèi)存的需要,而此時有可能 Arena 也并不適用。在這種情況下,善用 RAII(Resource Acquisition Is Initialization)思想會幫助我們解決很多問題。

簡單來說,RAII 可以幫助我們將管理堆上的內(nèi)存,簡化為管理棧上的內(nèi)存,從而達到利用編譯器自動解決內(nèi)存回收問題的效果。此外,RAII 可以簡化的還不僅僅是內(nèi)存管理,還可以簡化對資源的管理,例如 fd,鎖,引用計數(shù)等等。

當我們需要在堆上分配內(nèi)存時,我們可以同時在棧上面分配一個對象,讓棧上面的對象對堆上面的對象進行封裝,用時通過在棧對象的析構(gòu)函數(shù)中釋放堆內(nèi)存的方式,將棧對象的生命周期和堆內(nèi)存進行綁定。

unique_ptr 就是一種很典型的例子。然而 unique_ptr 管理的對象類型只能是指針,對于其他的資源,例如 fd,我們可以通過將 fd 封裝成另外一個 FileHandle 對象的方式管理,也可以采用一些更通用的方式。例如,在我們內(nèi)部的 C++ 基礎(chǔ)庫中實現(xiàn)了 Defer 類,想法類似于 Go 中 defer。

void Foo() { int fd = open(); Defer d = [=]() { close(fd); } // do something with fd}

經(jīng)驗 #5:便于 Debug

在特定的情況下,我們難免還是要手動管理堆上的內(nèi)存。然而當我們面臨一個正在發(fā)生內(nèi)存泄露線上程序時,我們應(yīng)該怎么處理呢?

當然不是簡單的『重啟大法好』,畢竟重啟后還是可能會產(chǎn)生泄露,而且最寶貴的現(xiàn)場也被破壞了。最佳的方式,還是利用現(xiàn)場進行 Debug,這就要求程序具有便于 Debug 的能力。

這里不得不提到一個經(jīng)典而強大的工具 gperftools。gperftools 是 google 開源的一個工具集,包含了 tcmalloc,heap profiler,heap checker,cpu profiler 等等。gperftools 的作者之一,就是大名鼎鼎的 Sanjay Ghemawat,沒錯,就是與 Jeff Dean 齊名,并和他一起寫 MapReduce 的那個 Sanjay。

gperftools 的一些經(jīng)典用法,我們就不在這里進行介紹了,大家可以自行查看文檔。而使用 gperftools 可以在不重啟程序的情況下,進行內(nèi)存泄露檢查,這個恐怕是很少有人了解。

實際上我們 Release 版本的 C++ 程序可執(zhí)行文件在編譯時全部都鏈接了 gperftools。在 gperftools 的 heap profiler 中,提供了 HeapProfilerStart 和 HeapProfilerStop 的接口,使得我們可以在運行時啟動和停止 heap profiler。同時,我們每個程序都暴露了 RPC 接口,用于接收控制命令和調(diào)試命令。在調(diào)試命令中,我們就增加了調(diào)用 HeapProfilerStart 和 HeapProfilerStop 的命令。由于鏈接了 tcmalloc,所以 tcmalloc 可以獲取所有內(nèi)存分配和回收的信息。當 heap profiler 啟動后,就會定期的將程序內(nèi)存分配和回收的行為 dump 到一個臨時文件中。

當程序運行一段時間后,你將得到一組 heap profile 文件

profile.0001.heap profile.0002.heap 。。. profile.0100.heap每個 profile 文件中都包含了一段時間內(nèi),程序中內(nèi)存分配和回收的記錄。如果想要找到內(nèi)存泄露的線索,可以通過使用pprof --base=profile.0001.heap /usr/bin/xxx profile.0100.heap --text 來進行查看,也可以生成 pdf 文件,會更直觀一些。

這樣一來,我們就可以很方便的對線上程序的內(nèi)存泄露進行 Debug 了。

寫在最后

C++ 可謂是最復(fù)雜、最靈活的語言,也最容易給大家?guī)砝_。如果想要用好 C++,團隊必須保持比較成熟的心態(tài),團隊成員必須愿意按照一定的規(guī)則來使用 C++,而不是任性的隨意發(fā)揮。這樣大家才能把更多精力放在業(yè)務(wù)本身,而不是編程語言的特性上。
責任編輯人:CC

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

    關(guān)注

    8

    文章

    3025

    瀏覽量

    74060
  • 內(nèi)存泄漏
    +關(guān)注

    關(guān)注

    0

    文章

    39

    瀏覽量

    9218

原文標題:C++ 如何避免內(nèi)存泄露

文章出處:【微信號:gh_c472c2199c88,微信公眾號:嵌入式微處理器】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    C++內(nèi)存泄漏分析方法

    C++是一種非常流行的計算機編程語言,在使用的過程中容易出現(xiàn)內(nèi)存泄漏問題,而該問題往往難以識別。給出了一種對C++內(nèi)存泄漏問題進行分析的方法
    發(fā)表于 11-23 11:19 ?5次下載
    C++<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>分析<b class='flag-5'>方法</b>

    內(nèi)存泄漏的特點和類型

    在計算機科學中,內(nèi)存泄漏(memory leak)指由于疏忽或錯誤使程序未能釋放而造成不能再使用的內(nèi)存的情況。內(nèi)存泄漏并非指
    的頭像 發(fā)表于 06-20 10:58 ?2827次閱讀

    內(nèi)存泄漏的檢測方法

    內(nèi)存泄露(Memory leak)指的是,在程序中動態(tài)申請的內(nèi)存,在使用完后既沒有釋放,又無法被程序的其他部分訪問。內(nèi)存泄露是在開發(fā)大型程序中最令人頭疼的問題,以至于有人說,內(nèi)存泄露是
    的頭像 發(fā)表于 06-20 11:01 ?3156次閱讀

    內(nèi)存泄漏問題原理及檢視方法

    內(nèi)存泄漏問題很可能導(dǎo)致單板運行固定時間以后就復(fù)位,只能通過批量升級才能解決,實際影響不佳。本文通過介紹內(nèi)存泄漏問題原理及檢視方法,希望后續(xù)能
    的頭像 發(fā)表于 10-10 10:42 ?2554次閱讀

    簡述C語言中的內(nèi)存泄漏的原理及解決方法

    內(nèi)存泄漏的原理 只有在堆內(nèi)存里面才會發(fā)生內(nèi)存泄漏的問題,在棧內(nèi)存中不會發(fā)生
    的頭像 發(fā)表于 06-29 14:58 ?7293次閱讀
    簡述C語言中的<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>的原理及解決<b class='flag-5'>方法</b>

    如何使用ThreadLocal來避免內(nèi)存泄漏

    本次給大家介紹重要的工具ThreadLocal。講解內(nèi)容如下,同時介紹什么場景下發(fā)生內(nèi)存泄漏,如何復(fù)現(xiàn)內(nèi)存泄漏,如何正確使用它來避免
    的頭像 發(fā)表于 08-20 09:29 ?4242次閱讀
    如何使用ThreadLocal來<b class='flag-5'>避免</b><b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>

    什么是內(nèi)存泄漏?內(nèi)存泄漏有哪些現(xiàn)象

    內(nèi)存泄漏幾乎是很難避免的,不管是老手還是新手,都存在這個問題,甚至 Windows 與 Linux 這類系統(tǒng)軟件也或多或少存在著內(nèi)存泄漏。
    的頭像 發(fā)表于 09-05 17:24 ?9704次閱讀

    什么是內(nèi)存泄漏?如何避免JavaScript內(nèi)存泄漏

    JavaScript 代碼中常見的內(nèi)存泄漏的常見來源: 研究內(nèi)存泄漏問題就相當于尋找符合垃圾回收機制的編程方式,有效避免對象引用的問題。
    發(fā)表于 10-27 11:30 ?399次閱讀
    什么是<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>?如何<b class='flag-5'>避免</b>JavaScript<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>

    內(nèi)存泄漏如何避免

    的數(shù),那就是內(nèi)存溢出。 2. 內(nèi)存泄漏 內(nèi)存泄露 memory leak,是指程序在申請內(nèi)存后,無法釋放已申請的
    的頭像 發(fā)表于 11-10 11:04 ?749次閱讀
    <b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>如何<b class='flag-5'>避免</b>

    內(nèi)存泄漏會產(chǎn)生哪些后果

    內(nèi)存泄漏原因 內(nèi)存泄漏在C/C++這種不帶GC(Garbage Collection)的語言里,是一個經(jīng)常發(fā)生的問題。因為沒有GC,所以分配的內(nèi)存
    的頭像 發(fā)表于 11-10 15:06 ?791次閱讀
    <b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>會產(chǎn)生哪些后果

    線程內(nèi)存泄漏問題的定位

    記錄一個關(guān)于線程內(nèi)存泄漏問題的定位過程,以及過程中的收獲。 1. 初步定位 是否存在內(nèi)存泄漏:想到內(nèi)存
    的頭像 發(fā)表于 11-13 11:38 ?619次閱讀
    線程<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問題的定位

    如何發(fā)現(xiàn)內(nèi)存泄漏

    檢測兩個角度介紹在 Linux 環(huán)境進行內(nèi)存泄漏檢測的方法,并重點介紹靜態(tài)分析工具 BEAM、動態(tài)監(jiān)測工具 Valgrind 和 rational purify 的使用方法。相信通過本
    的頭像 發(fā)表于 11-13 15:41 ?604次閱讀

    內(nèi)存溢出與內(nèi)存泄漏:定義、區(qū)別與解決方案

    內(nèi)存溢出與內(nèi)存泄漏:定義、區(qū)別與解決方案? 內(nèi)存溢出和內(nèi)存泄漏是計算機科學中常見的問題,在開發(fā)和
    的頭像 發(fā)表于 12-19 14:10 ?2797次閱讀

    C語言內(nèi)存泄漏問題原理

    內(nèi)存泄漏問題只有在使用堆內(nèi)存的時候才會出現(xiàn),棧內(nèi)存不存在內(nèi)存泄漏問題,因為棧
    發(fā)表于 03-19 11:38 ?528次閱讀
    C語言<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問題原理

    如何檢測內(nèi)存泄漏

    檢測內(nèi)存泄漏是軟件開發(fā)過程中一項至關(guān)重要的任務(wù),它有助于識別和解決那些導(dǎo)致程序占用過多內(nèi)存資源,從而影響程序性能甚至導(dǎo)致程序崩潰的問題。以下將詳細闡述幾種常見的內(nèi)存
    的頭像 發(fā)表于 07-30 11:50 ?1908次閱讀