/****************************************************************/
/* 學習是合作和分享式的!
/* Author:Atlas /* 轉載請注明本文出處:
/****************************************************************/
上期內容回顧:
C++內存管理學習筆記(6)
3 內存洩漏 3.1 引入-一個內存洩漏的解決方案 3.2 如何對付內存洩漏 3.3 淺談c++內存洩漏及檢測工具
--------------------------------------------------------------------------------
3.3.3 檢測內粗洩漏的工具介紹
在《C/C++內存洩漏及檢測》一文中,作者簡單的介紹了下在Windows、Linux下面的內存檢測。這裡我將大致內容引用過來,增加對內存管理中內存洩漏部分的理解。目前筆者主要是在Linux下作一些開發應用,所主要說明在linux平台下的方式,其實這不同平台下原理相同形式不同而已。
首先,來看一個簡單的內存洩漏例子,雖然最後函數結束會被釋放掉,如果此時while(1),則內存會耗盡
1: #include <stdlib.h> 2: #include <iostream> 3: 4: usingnamespacestd; 5: 6: void GetMemory(char*p, intnum) 7: { 8: p = (char*)malloc(sizeof(char) * num);//使用new也能夠檢測出來 9: } 10: int main(intargc,char** argv) 11: { 12: char *str = NULL; 13: GetMemory(str, 100); 14: cout<<"Memory leak test!"<<endl; 15: //如果main中存在while循環調用GetMemory 16: //那麼問題將變得很嚴重 17: //while(1){GetMemory(...);} 18: return 0; 19: }
Linux平台下,原理理大致如下:要在分配內存和釋放內存時分別做好記錄,程序結束時對比分配內存和釋放內存的記錄就可以確定是不是有內存洩漏。此處作者推薦了linux下mtrace工具,以及另外一個非常強大的工具valgrind,
如上圖所示知道:
==6118== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==6118== at 0x4024F20: malloc (vg_replace_malloc.c:236)
==6118== by 0x8048724: GetMemory(char*, int) (in /home/netsky/workspace/a.out)
==6118== by 0x804874E: main (in /home/netsky/workspace/a.out)
是在main中調用了GetMemory導致的內存洩漏,GetMemory中是調用了malloc導致洩漏了100字節的內存。
Things to notice:
• There is a lot of information in each error message; read it carefully.
• The 6118 is the process ID; it’s usually unimportant.
• The first line ("Heap Summary") tells you what kind of error it is.
• Below the first line is a stack trace telling you where the problem occurred. Stack traces can get quite large, and be confusing, especially if you are using the C++ STL. Reading them from the bottom up can help.
• The code addresses (eg. 0x4024F20) are usually unimportant, but occasionally crucial for tracking down weirder bugs.The stack trace tells you where the leaked memory was allocated. Memcheck cannot tell you why the memory leaked, unfortunately. (Ignore the "vg_replace_malloc.c", that’s an implementation detail.) There are several kinds of leaks; the two most important categories are:
• "definitely lost": your program is leaking memory -- fix it!
• "probably lost": your program is leaking memory, unless you’re doing funny things with pointers (such as moving
them to point to the middle of a heap block)
3.4 關於內存回收這件事
內存回收是確保內存不洩露的有效機制。
(1)三種內存對象的分析
棧對象的優勢是在適當的時候自動生成,又在適當的時候自動銷毀,不需要程序員操心;而且棧對象的創建速度一般較堆對象快,因為分配堆對象時,會調用operator new操作,operator new會采用某種內存空間搜索算法,而該搜索過程可能是很費時間的,產生棧對象則沒有這麼麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,所以體積比較大的對象不適合在棧中分配。特別要注意遞歸函數中最好不要使用棧對象,因為隨著遞歸調用深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢出,這樣就會產生運行時錯誤。
堆對象,其產生時刻和銷毀時刻都要程序員精確定義,也就是說,程序員對堆對象的生命具有完全的控制權。我們常常需要這樣的對象,比如,我們需要創建一個對象,能夠被多個函數所訪問,但是又不想使其成為全局的,那麼這個時候創建一個堆對象無疑是良好的選擇,然後在各個函數之間傳遞這個堆對象的指針,便可以實現對該對象的共享。另外,相比於棧空間,堆的容量要大得多。實際上,當物理內存不夠時,如果這時還需要生成新的堆對象,通常不會產生運行時錯誤,而是系統會使用虛擬內存來擴展實際的物理內存。
接下來看看static對象:
首先是全局對象。全局對象為類間通信和函數間通信提供了一種最簡單的方式,雖然這種方式並不優雅。一般而言,在完全的面向對象語言中,是不存在全局對象的,比如C#,因為全局對象意味著不安全和高耦合,在程序中過多地使用全局對象將大大降低程序的健壯性、穩定性、可維護性和可復用性。C++也完全可以剔除全局對象,但是最終沒有,我想原因之一是為了兼容C。
其次是類的靜態成員,上面已經提到,基類及其派生類的所有對象都共享這個靜態成員對象,所以當需要在這些class之間或這些class objects之間進行數據共享或通信時,這樣的靜態成員無疑是很好的選擇。
接著是靜態局部對象,主要可用於保存該對象所在函數被屢次調用期間的中間狀態,其中一個最顯著的例子就是遞歸函數,我們都知道遞歸函數是自己調用自己的函數,如果在遞歸函數中定義一個nonstatic局部對象,那麼當遞歸次數相當大時,所產生的開銷也是巨大的。這是因為nonstatic局部對象是棧對象,每遞歸調用一次,就會產生一個這樣的對象,每返回一次,就會釋放這個對象,而且,這樣的對象只局限於當前調用層,對於更深入的嵌套層和更淺露的外層,都是不可見的。每個層都有自己的局部對象和參數。
在遞歸函數設計中,可以使用static對象替代nonstatic局部對象(即棧對象),這不僅可以減少每次遞歸調用和返回時產生和釋放nonstatic對象的開銷,而且static對象還可以保存遞歸調用的中間狀態,並且可為各個調用層所訪問。
(2)垃圾回收的幾個基本方法
本部分內容在《C/C++中幾種經典的垃圾回收算法》一文中作者提出了幾個垃圾回收的方式,分別是應用計數算法,標記-清除方法,標記-縮並方法
1.引用計數算法 --智能指針的使用
引用計數(Reference Counting)算法是每個對象計算指向它的指針的數量,當有一個指針指向自己時計數值加1;當刪除一個指向自己的指針時,計數值減1,如果計數值減為0,說明已經不存在指向該對象的指針了,所以它可以被安全的銷毀了。
引用計數算法的優點在於內存管理的開銷分布於整個應用程序運行期間,非常的“平滑”,無需掛起應用程序的運行來做垃圾回收;而它的另外一個優勢在於空間上的引用局部性比較好,當某個對象的引用計數值變為0時,系統無需訪問位於堆中其他頁面的單元,而後面我們將要看到的幾種垃圾回收算法在回收前都回遍歷所有的存活單元,這可能會引起換頁(Paging)操作;最後引用計數算法提供了一種類似於棧分配的方式,廢棄即回收,後面我們將要看到的幾種垃圾回收算法在對象廢棄後,都會存活一段時間,才會被回收。
引用計數算法有著諸多的優點,但它的缺點也是很明顯的。首先能看到的一點是時間上的開銷,每次在對象創建或者釋放時,都要計算引用計數值,這會引起一些額外的開銷;第二是空間上的開銷,由於每個對象要保持自己被引用的數量,必須付出額外的空間來存放引用計數值;引用計數算法最大的缺點就在於它無法處理環形引用,如果成環的兩個對象既不可達也無法回收,因為彼此之間互相引用,它們各自的計數值都不為0,這種情況對引用計數算法來說是無能為力的,而其他的垃圾回收算法卻能很好的處理環形引用。
引用計數算法最著名的運用,莫過於微軟的COM技術,大名鼎鼎的IUnknown接口:
1: interface IUnknown 2: { 3: virtual HRESULT _stdcall QueryInterface 4: (const IID& iid, void* * ppv) = 0; 5: virtual ULONG _stdcall AddRef() = 0; 6: virtual ULONG _stdcall Release() = 0; 7: }
其中的AddRef和Release就是用來讓組件自己管理其生命周期,而客戶程序只關心接口,而無須再去關心組件的生命周期,一個簡單的使用示例如下:
1: int main() 2: { 3: IUnknown* pi = CreateInstance(); 4: 5: IX* pix = NULL; 6: HRESULT hr = pi->QueryInterface(IID_IX, (void*)&pix); 7: if(SUCCEEDED(hr)) 8: { 9: pix->DoSomething(); 10: pix->Release(); 11: } 12: 13: pi->Release(); 14: }
上面的客戶程序在CreateInstance中已經調用過AddRef,所以無需再次調用,而在使用完接口後調用Release,這樣組件自己維護的計數值將會改變。下面代碼給出一個簡單的實現AddRef和Release示例:
1: ULONG _stdcall AddRef() 2: { 3: return ++ m_cRef; 4: } 5: 6: ULONG _stdcall Release() 7: { 8: if(--m_cRef == 0) 9: { 10: delete this; 11: return 0; 12: } 13: return m_cRef; 14: }
在編程語言Python中,使用也是引用計數算法,當對象的引用計數值為0時,將會調用__del__函數,至於為什麼Python要選用引用計數算法,據我看過的一篇文章裡面說,由於Python作為腳本語言,經常要與C/C++這些語言交互,而使用引用計數算法可以避免改變對象在內存中的位置,而Python為了解決環形引用問題,也引入gc模塊,所以本質上Python的GC的方案是混合引用計數和跟蹤(後面要講的三個算法)兩種垃圾回收機制。
2.標記-清除算法
標記-清除(Mark-Sweep)算法依賴於對所有存活對象進行一次全局遍歷來確定哪些對象可以回收,遍歷的過程從根出發,找到所有可達對象,除此之外,其它不可達的對象就是垃圾對象,可被回收。整個過程分為兩個階段:標記階段找到所有存活對象;清除階段清除所有垃圾對象。
標記階段
清除階段
相比較引用計數算法,標記-清除算法可以非常自然的處理環形引用問題,另外在創建對象和銷毀對象時時少了操作引用計數值的開銷。它的缺點在於標記-清除算法是一種“停止-啟動”算法,在垃圾回收器運行過程中,應用程序必須暫時停止,所以對於標記-清除算法的研究如何減少它的停頓時間,而分代式垃圾收集器就是為了減少它的停頓時間,後面會說到。另外,標記-清除算法在標記階段需要遍歷所有的存活對象,會造成一定的開銷,在清除階段,清除垃圾對象後會造成大量的內存碎片。