createbitmap導致的內存泄漏如何處理_C++ 如何避免內存泄漏,一篇就夠

前言

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

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

619d7184635aa4c21423d9393cfd2075.png

也就是說,如果你的應用場景可以容忍不定期的 500us 的延遲,那么用 Go 都是沒有問題的。如果你無法容忍 500us 的延遲,那么帶 GC 功能的語言就基本無法使用了,只能選擇自己管理內存的語言,例如 C++。那么由手動管理內存而帶來的編程復雜度也就隨之而來了。

作為 C++ 程序員,內存泄露始終是懸在頭上的一顆炸彈。在過去幾年的 C++ 開發過程中,由于我們采用了一些技術,我們的程序發生內存泄露的情況屈指可數。今天就在這里向大家做一個簡單的介紹。

內存是如何泄露的

在 C++ 程序中,主要涉及到的內存就是『棧』和『堆』(其他部分不在本文中介紹了)。

ebc7baeebf155c251cf6b2c1d41f0867.png

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

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

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

經驗 #1:盡量避免在堆上分配內存

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

然而,只在棧上分配內存,在有 IO 的情況下是存在一定局限性的。

舉個例子,為了完成一個請求,我們通常會為這個請求構造一個 Context 對象,用于描述和這個請求有關的一些上下文。例如下面一段代碼:

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

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

但如果 HandleRequest 是一個異步函數,例如:

void HandleRequest(RequestContext* ctx, Callback cb);

那么顯然,ctx 是不能被分配在棧上的,因為如果 ctx 被分配在棧上,那么當 Foo 函數推出后,ctx 對象的生命周期也就結束了。而 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 中調用 delete ctx,則就會觸發內存泄露。盡管我們可以借助一些靜態檢查工具對代碼進行檢查,但往往異步程序的邏輯是極其復雜的,一個請求的生命周期中,也需要進行大量的內存分配操作,靜態檢查工具往往無法發現所有的內存泄露情況。

那么怎么才能避免這種情況的產生呢?引入智能指針顯然是一種可行的方法,但引入 shared_ptr 往往引入了額外的性能開銷,并不十分理想。

在 SmartX,我們通常采用兩種方法來應對這種情況。

經驗 #2:使用 Arena

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

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

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

經驗 #3:使用 Coroutine

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

有了多個棧,意味著當我們要做一個異步處理時,不需要釋放當前棧上的內存,而只需要切換到另一個棧上,就可以繼續做其他的事情了,當異步處理完成時,可以再切換回到這個棧上,將這個請求處理完成。

還是以剛才的代碼為示例:

void Foo(Reuqest* req) {  RequestContext ctx(req);  HandleRequest(&ctx); } void HandleRequest(RequestCtx* ctx) {  SubmitAsync(ctx);  Coroutine::Self()->Yield();  CompleteRequest(ctx); }

這里的精髓在于,盡管 Coroutine::Self()->Yield() 被調用時,程序可以跳出 HandleRequest 函數去執行其他代碼邏輯,但當前的棧卻被保存了下來,所以 ctx 對象是安全的,并沒有被釋放。

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

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

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

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

0b1411f2ac507770a61e46ad0160f88f.png

經驗 #4:善用 RAII

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

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

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

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

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

經驗 #5:便于 Debug

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

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

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

b308481f9a9d11d6da4564b53e7d6953.png

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

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

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

profile.0001.heap

profile.0002.heap

...

profile.0100.heap

每個 profile 文件中都包含了一段時間內,程序中內存分配和回收的記錄。如果想要找到內存泄露的線索,可以通過使用

pprof --base=profile.0001.heap /usr/bin/xxx profile.0100.heap --text

來進行查看,也可以生成 pdf 文件,會更直觀一些。

8a76f5e6c1e91ae5dad2446b41c305ec.png

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

寫在最后

C++ 可謂是最復雜、最靈活的語言,也最容易給大家帶來困擾。如果想要用好 C++,團隊必須保持比較成熟的心態,團隊成員必須愿意按照一定的規則來使用 C++,而不是任性的隨意發揮。這樣大家才能把更多精力放在業務本身,而不是編程語言的特性上。

如果你在C/C++以及后臺服務開發方面有什么困惑的話,我們建了一個QQ群:762073882,謝絕廣告。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/259772.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/259772.shtml
英文地址,請注明出處:http://en.pswp.cn/news/259772.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Visio繪制功能分解圖

為什么要繪制功能分解圖? 對于編程人員來說,具體分配任務的時候,必須知道自己要做什么,必須了解系統的大體框架。功能分解圖可以幫助我們理清程序的框架,便于大局觀的掌握。 用Visio2010創建功能分解圖 1、選擇模版 2、…

Heka:Go編寫,來自Mozilla,高效、靈活的插件式數據挖掘工具(轉)

轉自:http://www.csdn.net/article/2013-05-02/2815116-introduce-from-mozilla-heka-go摘要:一直崇尚開源的Mozilla近日釋放了Heka測試版——插件架構,Go編寫。在支持使用Go擴展功能的同時,還通過允許“Sandboxed Filters”提供了…

cocos2d學習筆記2——學習資源

1. 視頻 找了好幾個視頻,有一些講得好的文件資源沒有,后來終于找到一個講得不錯還有文件資源的,還有高清下載地址,雖然是2.2版本的,但是確實能學到不少東西,對用cocos2d做游戲有了基本的印象,對…

深究標準IO的緩存

前言 在最近看了APUE的標準IO部分之后感覺對標準IO的緩存太模糊,沒有搞明白,APUE中關于緩存的部分一筆帶過,沒有深究緩存的實現原理,這樣一本被吹上天的書為什么不講透徹呢?今天早上爬起來趕緊找了幾篇文章看看&#x…

環境變量_配置JAVA環境變量

本文標識 : J00001本文編輯 : YiKi編程工具 : IDEA閱讀時長 : 3分鐘什么是環境變量?環境變量是在操作系統中一個具有特定名字的對象, 它包含了一個或者多個應用程序所將使用到的信息。為什么要配置環境變量?為了方便在控制臺編譯和運行java程序,不…

GotFocus和PreviewLeftButtonDown事件

當TextBox獲得焦點后,其中的文字會被全選。通過GotFocus和PreviewLeftButtonDown事件,就可以模擬上述行為。 如果用戶只是用鍵盤操作,GotFocus事件就足夠了。 如果使用鼠標操作,就要用到2個事件了。TextBox會將光標放在鼠標單擊的…

模式主節點ORACLE DG介紹(物理無實例)

在本文中,我們主要介紹模式主節點的內容,自我感覺有個不錯的建議和大家分享下 DG的三種模式: 硬件以及操縱系統需求: 每日一道理 流逝的日子像一片片凋零的枯葉與花瓣,漸去漸遠的是青春的純情與浪漫。不記得曾有多少雨飄在胸前風響在耳畔&…

分布式消息隊列 Kafka

分布式消息隊列 Kafka 2016-02-25 杜亦舒Kafka是一個高吞吐量的、分布式的消息系統,由Linkedin開發,開發語言為scala具有高吞吐、可擴展、分布式等特點 適用場景 活動數據統計活動數據包括頁面訪問量(Page View)、被查看內容方面的…

漫游飛行_手機“飛行模式”為何沒被淘汰?內行人坦言:其實是你不會用!

隨著科技的不斷創新,目前市面上出現的手機款式多種多樣,品牌也非常多,有華為、蘋果、三星和小米等等。手機的屏幕也是五花八門,有劉海屏、水滴全面屏等,這些屏幕之間都各有不同。而且手機的更新換代速度很快&#xff0…

multiselect多選下拉框

具體實現 <input type"hidden" id"q_dueDay" name"q_dueDay" value"${baseQueryBean.q_dueDay}">//這個為隱藏域后臺直接使用這個為參數 <select id"example" name"example" multiple"multiple&qu…

序列元素IT面試題——判斷合法出棧序列

本文純屬個人見解&#xff0c;是對前面學習的總結&#xff0c;如有描述不正確的地方還請高手指正~ 在技巧筆試口試上&#xff0c;我們常常會碰到這樣一類題型&#xff0c;如給你一個入棧序列&#xff0c;然后再讓你判斷幾個序列是否有可能為它的出棧序列&#xff0c;如&#xf…

scikit-learn點滴

scikit-learn點滴 scikit-learn是非常漂亮的一個機器學習庫,在某些時候,使用這些庫能夠大量的節省你的時間,至少,我們用Python,應該是很難寫出速度快如斯的代碼的. scikit-learn官方出了一些文檔,但是個人覺得,它的文檔很多東西都沒有講清楚,它說算法原理的時候,只是描述一下,除…

background image

http://www.ajaxblender.com/bgstretcher-2-jquery-stretch-background-plugin-updated.html http://blog.dvxj.com/pandola/jQuery_bgStretcher.html 轉載于:https://www.cnblogs.com/eebb/p/4077231.html

怎樣搭建Android開發平臺(轉)

Android是基于Linux內核的軟件平臺和操作系統&#xff0c;是Google在2007年11月5日公布的手機系統平臺&#xff0c;早期由Google開發&#xff0c;后由開放手機聯盟&#xff08;Open Handset Alliance&#xff09;開發。 它采用了軟件堆層&#xff08;software stack&#xff0c…

mvn deploy 推送到私有倉庫,注意當前日期

由于更改了本機系統時間到過去的一個時間&#xff0c;導致mvn deploy推送到私有倉庫后&#xff0c;該更新的jar包時間戳比較舊&#xff0c;客戶端不能更新得到新的jar包。轉載于:https://www.cnblogs.com/silva/p/6264458.html

我的世界1.7.10java32位_我的世界1.7.10中文版

不知道怎么下載&#xff1f;點我游戲介紹《我的世界1.7.10》中整個世界由各種方塊構成&#xff0c;玩家可以破壞它們&#xff0c;也可以用自己的方塊隨意建造東西。為了在游戲里生存和發展&#xff0c;玩家需要通過伐木、挖礦、捕獵等方式獲取資源&#xff0c;并通過合成系統打…

python程序在函數內執行得更快

http://www.cnblogs.com/nepaul/archive/2012/07/15/2592179.html 為什么Python程序在函數內執行得更快&#xff1f;&#xff08;來源StackOverflow&#xff09; 考慮下面的代碼&#xff0c;一個在函數體內&#xff0c;一個是全局的代碼。 函數內的代碼執行效率為 1.8s 1234def…

USER_EXIT

1、md04的用戶出口 M61X0002 2、me21n/me22n的用戶出口 MM06E005 MBCF0002 3、migo 的用戶出口&#xff1a; MBCF0009 MBCF0002-> EXIT_SAPMM07M_001 4、co11n 的用戶出口&#xff0c;發料不足不允許報工時 EXIT_SAPLCORF_104 查找用戶出口的函數&#xff1a; MODX_FUNCTION…

subject.login(token)是如何確認賬號密碼的_教你如何刪除、關閉、注銷微信小程序...

微信小程序是我們日常生活中經常會接觸到的工具&#xff0c;打開小程序后&#xff0c;它就會留在我們微信的”“發現-小程序”欄。很多人并不知道該如何刪除、關閉小程序&#xff0c;所以今天就跟大家科普下相關問題。1.如何刪除小程序首先&#xff0c;打開微信界面&#xff0c…

上海交通大學2006年數學分析考研試題

轉載于:https://www.cnblogs.com/zhangzujin/p/4078900.html