一、C++內(nèi)存管理
C++中有四種內(nèi)存分配、釋放方式:
最高級的是std::allocator
,對應(yīng)的釋放方式是std::deallocate
,可以自由設(shè)計來搭配任何容器;new/delete
系列是C++函數(shù),可重載;malloc/free
屬于C++表達式,不可重載;更低級的內(nèi)存管理函數(shù)是操作系統(tǒng)直接提供的系統(tǒng)調(diào)用,通常不會到這個層次來寫C++應(yīng)用程序。接下來的闡述集中在上三層。
現(xiàn)在讓我們寫一些示例:
// c語言中的malloc/free void *p1 = malloc(512); *(int*) p1 = 100; free(p1); // c++表達式new/delete int *p2 = new int(100);// 這里應(yīng)該是初始化為100,若要聲明數(shù)組,用new int[n]; delete p2; // c++函數(shù),等價于malloc和free void *p3 = ::operator new(512); *(int *)p3 = 100; ::operator delete(p3); // c++標(biāo)準庫,通過object調(diào)用,以GNU為例 int *p4 = alloctor<int>().allocate(7);// 這里是分配7個int單元而不是7bytes *p4 = 9; allocator<int>().deallocate((int *)p4,7);// 7應(yīng)該與上文匹配
1、 new/delete表達式
new表達式的內(nèi)部實現(xiàn)是由三個步驟組成的,首先調(diào)用operator new
分配一定的字節(jié)數(shù)的內(nèi)存,此時得到的內(nèi)存指針是void*類型的,將之用static_cast
轉(zhuǎn)換成需要的class
類型,然后調(diào)用class
的構(gòu)造函數(shù)完成初始化。
Complex *pc; try { void* mem = operator new(sizeof(Complex)); // 分配內(nèi)存 pc = static_cast<Complex*>(mem); // cast 轉(zhuǎn)型 以符合對應(yīng)的類型,這里對應(yīng)為Complex* pc->Complex::Complex(1,2); // construct // 注意:只有編譯器才可以像上面那樣直接呼叫constructor 欲直接調(diào)用constructor可通用placement new: new(p) Complex(1,2); } catch(std::bad_alloc) { // 若allocation失敗就不執(zhí)行constructor }
從上面的new
的實現(xiàn)中,不難發(fā)現(xiàn),new返回的是對象指針,失敗是拋出bad_alloc
異常,我們應(yīng)該通過是否拋出異常來判斷new執(zhí)行狀況,而不是像malloc
一樣通過判斷返回值是否為nullptr
。
對于delete
,其具體操作與new
相反,先調(diào)用對象的析構(gòu)函數(shù),再釋放內(nèi)存。
// delete pc; pc->~Complex(); //先析構(gòu) operator delete(pc); //然后釋放內(nèi)存
array new
和array delete
可以獲取一組對象,new的時候不能同時初始化,因此通常結(jié)合placement new
來創(chuàng)建對象。
class A{ public: int id; A(): id(0){} A(int i): id(i){} ~A(){} }; A* buf = new A[size]; // 調(diào)用三次默認構(gòu)造函數(shù)A():id(0) A* tmp = buf; // placement new指定位置new for(int i = 0; i<size; i++) new(tmp++) A(i); //調(diào)用A(int i): id(i)自定義初始化 delete []buf; // 調(diào)用三次~A()
new array
的構(gòu)造是從0到size-1
依次構(gòu)造,析構(gòu)的時候恰恰相反(但這不重要)。new array
返回的指針指向第0個對象的起始位置。
數(shù)組的new
必須和數(shù)組的delete
聯(lián)合使用,否則可能發(fā)生內(nèi)存泄漏:若只使用delete
而非delete []
,則只會將分配的size塊內(nèi)存空間釋放,但是不會調(diào)用對象的析構(gòu)函數(shù)(可能是因為此處將size塊對象視為一個對象之后,找不到合適的析構(gòu)函數(shù)),沒有析構(gòu)就釋放內(nèi)存是很不優(yōu)雅的,如果對象內(nèi)部還使用了new
指向其他空間,那么這部分空間不會被釋放。如果new array
分配的是一些析構(gòu)函數(shù)沒有意義的對象
比如:
int* pi = new int[10]; delete pi;
那么是完全沒有問題的,delete
等價于delete[] 。
附圖:聲明了3個對象的時候的內(nèi)存分布,其中cookie
保存著數(shù) 組大小等信息。
2、new/delete重載
C++ new的調(diào)用鏈是圖中的2,operator new
將在全局環(huán)境下尋找匹配的函數(shù)。如果要重構(gòu),則最好在此處將其轉(zhuǎn)為調(diào)用類內(nèi)自定義的Foo::operator new
,在Foo::operator new
中再調(diào)用::operator new
。已經(jīng)定義了重載之后,也可以通過直接調(diào)用::operator new
來繞過重載。重載的原則是,盡量在高層、部分可見的局部進行重載,使影響盡可能小而且可控。繪制函數(shù)調(diào)用鏈可以很好地幫助決定重載層次。除了new之外,new array
等也可以重載。
3、類內(nèi)自定義allocator(per-class allocator)
本節(jié)介紹的是一個類內(nèi)借助內(nèi)存池的內(nèi)存管理。雖然malloc不慢,但減少malloc
調(diào)用次數(shù)總是好的;此外,一次malloc得到的內(nèi)存塊前總是帶有一個cookie
,它占有8個字節(jié)。基于以上兩個原因,從時間和空間的角度看,建立內(nèi)存池都是有必要的。
思考:當(dāng)每次alloc的時候都alloc固定大小的一大塊的時候,應(yīng)該更難以產(chǎn)生外部碎片(雖然可能更容易產(chǎn)生內(nèi)部碎片),而且固定大小對于OS的高級分配器來說是十分友好的。
這里直接看per-class allocator3。
-
embedded pointer
和類型轉(zhuǎn)換 - 鏈表管理的內(nèi)存池
- 抽象的思想
-
if
判斷將值放在變量前面,這樣可以避免少寫等號,編譯器不報錯問題,例如if(1!=p){}
。
#include <iostream> #include <complex> using namespace std; class my_allocator{ private: struct obj{ struct obj* next; // embedded pointer }; obj* freestore = nullptr; const int CHUNK = 5; public: my_allocator(){}; ~my_allocator(){}; void* allocate(size_t); void deallocate(void*, size_t); }; void* my_allocator::allocate(size_t size){ // 從內(nèi)存池分配一個obj對象大小的內(nèi)存 assert(size>0); obj* p; if(!freestore){ freestore = p = static_cast<obj*>(malloc(CHUNK*size)); for(int i = 0;i<CHUNK-1;i++){ p->next = (obj*)((char*)(p+size)); p = p->next; } p->next = nullptr; } p = freestore; freestore = freestore->next; return p; } void deallocate(void* p, size_t size){ // 插入到內(nèi)存池 (static_cast<obj*>(p))->next = freestore; freestore = static_cast<obj*>(p); } // example class Foo{ public: long L; string str; static my_allocator myAlloc; Foo(long l): L(l){} static void* operator new(size_t size){ return myAlloc.allocate(size); } static void operator delete(void* dead, size_t size){ return myAlloc.deallocate(dead, size); } }; my_allocator Foo::myAlloc; // 靜態(tài)成員變量一定要在類聲明之外定義
以下討論GNU
編譯器中的內(nèi)存管理機制。
allocator
是普通的分配器,它通過operator new
和operator delete
調(diào)用malloc
和free
,沒有特殊的設(shè)計。
G4.9的__pool_alloc
(相當(dāng)于G2.9的std::alloc)是在容器中使用的分配器,是利用上了內(nèi)存池的分配器。std::alloc
使用一個16個寫代指針頭的數(shù)組來管理內(nèi)存鏈表,數(shù)組的不同元素管理不同大小的區(qū)塊,每種區(qū)塊大小相差8個字節(jié)。內(nèi)存首先由malloc
分配到戰(zhàn)備池pool中,再從戰(zhàn)備池挖適當(dāng)?shù)目臻g到鏈表。假設(shè)用戶需要32字節(jié)的內(nèi)存,std::alloc
首先申請一塊區(qū)間,大小為32*20*2,用一條鏈表管理,然后讓數(shù)組的#3指針管理這條鏈表,接著將其中一個單元(32字節(jié))分給用戶。這32*20*2中,一半是給用戶的,后一半預(yù)留在戰(zhàn)備池中,如果此時用戶需要一個64字節(jié)的空間,那么剩下的一半將變成64*10(通常是申請64*20),由另一個鏈表指針指向這里,然后將其中64字節(jié)分配給用戶,而不用再一次構(gòu)建鏈表和申請空間。鏈表數(shù)組維護的鏈表最大塊是128字節(jié),如果申請超過了這個大小,那么直接調(diào)用malloc給用戶分配,這樣每一塊都會帶上cookie頭和尾。
- 戰(zhàn)備池,池中內(nèi)存沒有固定塊大小
- 多級大小內(nèi)存池鏈表
- 兩級分配器:超過最大大小直接使用malloc分配
G2.9中的一級配置器主要是對malloc和free進行了一些封裝,當(dāng)申請的內(nèi)存較大的時候,二級分配器將直接調(diào)用一級分配器。一級分配器在G4.9中已經(jīng)棄用。此處不再過多闡述。
二級配置器執(zhí)行分配器的主要功能。流程圖和部分源碼如下。
static const int __ALLGN = 8; // 上調(diào)邊界 static const int __MAX_BYTES = 8; // 分配Chunk的上限 static const int __NFREELISTS = __MAX_BYTES/__ALLGN; // 鏈表的條數(shù) template<bool threads, int inst> class __default_alloc_template{ private: static size_t ROUND_UP(size_t bytes){ // 向上取整8 return (bytes+__ALLGN-1) & ~(__ALLGN-1); } union obj{ // 亦可用struct union obj* free_list_link; // 鏈表的next指針,老規(guī)矩用了ebedded pointer } static obj* volatile free_list[__NFREELISTS]; // 多級大小內(nèi)存池 static size_t FREELIST_INDEX(size_t bytes){ // 根據(jù)大小確定鏈表index return ((bytes+ALLGN-1)/__ALLGN-1); } static void *refill(size_t size); static char* chunk_alloc(size_t size, int &nobjs); // 戰(zhàn)備池 static char* start_free; // 指向pool的頭 static char* end_free; // 指向pool的尾 static size_t heap_size; // 分配累積量 public: static void* allocate(size_t size){ obj* volatile *my_free_list; // 鏈表的鏈表 obj* result; if(size > (size_t)__MAX_BYTES) // 大于128改用第一級分配器 return (malloc_alloc::allocate(size)); my_free_list = free_list+FREELIST_INDEX(size); result = *my_free_list; if(0==result){ void* t = refill(ROUND_UP(size)); // 對此鏈表充值 return t; } *my_free_list = result->free_list_link; return result; } static deallocate(void* p, size_t size){ obj* q = (obj*)p; obj* volatile* my_free_list; if(size > static_cast<size_t>(__MAX_BYTES)){ malloc_alloc::deallocate(p,size); // 大于128改用第一級分配器 return; } my_free_list = free_list + FREELIST_INDEX(size); q->free_list_link = *my_free_list; *my_free_list = q; } static void* reallocate(void* p, size_t old_size,size_t new_size); } /* We allocate memory in large chunks inn order to avoid fragmenting the malloc heap too much, We assume that size is properly aligned. We hold the allocation lock. */ template<bool threads, int inst> char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) { char* result; size_t total_bytes = size * nobjs; size_t bytees_left = end_free - start_free; if(bytees_left >= total_bytes) { //pool空間足以滿足需求 result = start_free; start_free += total_bytes; // 【Q1:如果pool中的空間不連續(xù)還能直接分配和相加嗎?A:虛擬地址是連續(xù)的】 return(result); }else if(bytees_left >= size) { //pool空間只滿足一塊以上 nobjs = bytees_left / size; //改變需求個數(shù) total_bytes = size * nobjs; //改變需求總量 pass-by-value會改變參數(shù) result = start_free; start_free += total_bytes; return (result); }else { //pool空間不足以滿足一塊需求 碎片&&0 //打算從system free-store上去這么多來充值 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); //處理碎片(將其掛到相應(yīng)的chunk指針端口) if(bytes_to_get > 0) { obj* volatile *my_free_list = //重新定位碎片的指針 free_list + FREELIST_INDEX(bytees_left); ((obj*)start_free)->free_list_link = *my_free_list; *my_free_list = (obj*)start_free; } //從system free-store中取 start_free = (char*)malloc(bytes_to_get); if(0 == start_free) { //如果當(dāng)前的chunk分配失敗,則向上繼續(xù)找相鄰的chunk繼續(xù)分配 obj* volatile *my_free_list, *p; for(int i = size; i <= __MAX_BYTES; i += __ALLGN) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if(0 != p) { //該free-list有可用區(qū)塊 *my_free_list = p->free_list_link; start_free = (char*)p; end_free = start_free + i; return (chunk_alloc(size, nobjs)); //結(jié)果再試一次 } } end_free = 0; start_free = (char*)malloc_alloc::allocate(bytes_to_get); } //至此,表示已經(jīng)從system free-store成功取得很多memory heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return (chunk_alloc(size, nobjs)); //戰(zhàn)備池有內(nèi)存了,所以遞歸重新處理分配邏輯 } } //靜態(tài)定義(分配內(nèi)存) template<bool threads, int inst> char* __default_alloc_template<threads, inst>::start_free = 0; template<bool threads, int inst> char* __default_alloc_template<threads, inst>::end_free = 0; template<bool threads, int inst> size_t __default_alloc_template<threads, inst>::heap_size = 0; template<bool threads, int inst> __default_alloc_template<threads, inst>::obj* volatile __default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; //std::alloc為第二級分配器 typedef __default_alloc_template<false, 0> alloc; int main(void){ std::vector<int,MyAllocator<int>> v; }
二、多線程內(nèi)存分配器
__pool_alloc:For thread-enabled configurations, the pool is locked with a single big lock.
mt_alloc:
使用了全局鏈表,分配到線程時移動到線程專享鏈表,在此過程中,只對鏈表的一個bin加鎖。exponentially-increasing allocations
。
tips
:盡量減小鎖的粒度
1、malloc/free
malloc/free
是 libc實現(xiàn)的庫函數(shù),主要實現(xiàn)了一套內(nèi)存管理機制,當(dāng)其管理的內(nèi)存不夠時,通過brk/mmap
等系統(tǒng)調(diào)用向內(nèi)核申請進程的虛擬地址區(qū)間,如果其維護的內(nèi)存能滿足malloc調(diào)用,則直接返回,free
時會將地址塊返回空閑鏈表。
malloc(size) 的時候,這個函數(shù)會多分配一塊空間,用于保存size變量,free的時候,直接通過指針前移一定大小,就可以獲取malloc時保存的size變量,從而free只需要一個指針作為參數(shù)就可以了calloc
庫函數(shù)相當(dāng)于 malloc + memset(0)
malloc和free碎片化嚴重(內(nèi)存站崗),在高并發(fā)下性能低下。除了libc自帶的動態(tài)內(nèi)存管理庫malloc, 有時候還可以使用其他的內(nèi)存管理庫替換,比如使用google實現(xiàn)的tcmalloc
,只需要編譯進程時鏈接上 tcmalloc
的靜態(tài)庫并包含響應(yīng)頭文件,就可以透明地使用tcmalloc
了,與libc 的malloc
相比, tcmalloc 在內(nèi)存管理上有很多改進,效率和安全性更好。
2、brk和mmap
在Linux下,glibc
的malloc
提供了下面兩種動態(tài)內(nèi)存管理的方法:堆內(nèi)存分配和mmap
的內(nèi)存分配,此兩種分配方法都是通過相應(yīng)的Linux 系統(tǒng)調(diào)用來進行動態(tài)內(nèi)存管理的。具體使用哪一種方式分配,根據(jù)glibc的實現(xiàn),主要取決于所需分配內(nèi)存的大小。一般情況中,應(yīng)用層面的內(nèi)存從進程堆中分配,當(dāng)進程堆大小不夠時,可以通過系統(tǒng)調(diào)用brk來改變堆的大小,但是在以下情況,一般由mmap
系統(tǒng)調(diào)用來實現(xiàn)應(yīng)用層面的內(nèi)存分配:A、應(yīng)用需要分配大于1M的內(nèi)存,B、在沒有連續(xù)的內(nèi)存空間能滿足應(yīng)用所需大小的內(nèi)存時。
(1)、調(diào)用brk實現(xiàn)進程里堆內(nèi)存分配
在glibc中,當(dāng)進程所需要的內(nèi)存較小時,該內(nèi)存會從進程的堆中分配,但是堆分配出來的內(nèi)存空間,系統(tǒng)一般不會回收,只有當(dāng)進程的堆大小到達最大限額時或者沒有足夠連續(xù)大小的空間來為進程繼續(xù)分配所需內(nèi)存時,才會回收不用的堆內(nèi)存。在這種方式下,glibc
會為進程堆維護一些固定大小的內(nèi)存池以減少內(nèi)存碎片。
(2)、使用mmap的內(nèi)存分配(堆和棧中間,稱為“文件映射區(qū)域”的地方)
在glibc
中,一般在比較大的內(nèi)存分配時使用mmap系統(tǒng)調(diào)用,它以頁為單位來分配內(nèi)存的(在Linux中,一般一頁大小定義為4K),這不可避免會帶來內(nèi)存浪費,但是當(dāng)進程調(diào)用free釋放所分配的內(nèi)存時,glibc會立即調(diào)用unmmap
,把所分配的內(nèi)存空間釋放回系統(tǒng)。
注意: 這里我們討論的都是虛擬內(nèi)存的分配(即應(yīng)用層面上的內(nèi)存分配),主要由glibc來實現(xiàn),它與內(nèi)核中實際物理內(nèi)存的分配是不同的層面,進程所分配到的虛擬內(nèi)存可能沒有對應(yīng)的物理內(nèi)存。如果所分配的虛擬內(nèi)存沒有對應(yīng)的物理內(nèi)存時,操作系統(tǒng)會利用缺頁機制來為進程分配實際的物理內(nèi)存。
默認情況下,malloc
函數(shù)分配內(nèi)存,如果請求內(nèi)存大于128K(可由M_MMAP_THRESHOLD
選項調(diào)節(jié)),那就不是去推_edata指針了,而是利用mmap
系統(tǒng)調(diào)用,從堆和棧的中間分配一塊虛擬內(nèi)存。
這樣子做主要是因為brk分配的內(nèi)存需要等到高地址內(nèi)存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,因為只有一個_edata 指針,這就是內(nèi)存碎片產(chǎn)生的原因)(圖2緊縮),而mmap分配的內(nèi)存可以單獨釋放。
malloc當(dāng)最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD
選項調(diào)節(jié))時,執(zhí)行內(nèi)存緊縮操作(trim)
缺頁中斷:
- 陷入內(nèi)核態(tài)
- 檢查要訪問的虛擬地址是否合法
- 查找/分配一個物理頁[......](buddy+slab)
- 填充物理頁內(nèi)容(讀取磁盤,或者直接置0,或者什么都不做)
- 建立映射關(guān)系(虛擬地址到物理地址的映射關(guān)系)
- 重復(fù)執(zhí)行發(fā)生缺頁中斷的那條指令
三、補充知識
1、內(nèi)存泄漏
內(nèi)存泄露是很隱蔽的錯誤,通常少量的內(nèi)存泄露不會造成什么問題,大量的內(nèi)存泄露可能會有“out of memory(OOM)”錯誤。
內(nèi)存泄露的檢測通常借助于內(nèi)存分析工具;( valgrind
或 purify
)
一般如果是簡單的 new 之后,沒有 delete
,這種泄漏最容易發(fā)現(xiàn)。真實場景可能比這復(fù)雜得多。有時候定位了相應(yīng)的函數(shù),但是代碼比較復(fù)雜,還是找不到泄漏點,可以參考如下幾個地方:
map
:c++的map
,在下標(biāo)訪問的時候自動構(gòu)造 value 對象,可能造成 map
無限增長;
unordered_set
: 在插入大量的元素之后,再刪除,內(nèi)存占用保持不變,需要手動 rehash
;
容器的 size 很大:通過 gcore -o xxx pidof yyy
,然后 gdb 去查看有嫌疑的容器的長度;
如果容器的 size 正常,但是還是有泄漏,可能跟智能指針有關(guān),例如 shared ptr,被泄漏;
2、malloc/free和new/delete的比較
3、RAII規(guī)則
RAII是指C++語言中的一個慣用法(idiom
),它是“Resource Acquisition Is Initialization
”的首字母縮寫。中文可將其翻譯為“資源獲取就是初始化”。
需要動態(tài)獲取和釋放的都可以稱為“資源”;
獲取資源和釋放資源要對應(yīng),這里就會面臨麻煩:釋放的不徹底將會導(dǎo)致memory leak
,致使程序臃腫、出錯等。
看到這里自然而然的可以想到C++中的一對特殊函數(shù),構(gòu)造函數(shù)和析構(gòu)函數(shù)。在構(gòu)造函數(shù)中申請資源,以及在析構(gòu)函數(shù)中釋放資源。
類是C++中的主要抽象工具,那么就將資源抽象為類,用局部對象來表示資源,把管理資源的任務(wù)轉(zhuǎn)化為管理局部對象的任務(wù)。這就是RAII慣用法,RAII有效地實現(xiàn)了C++資源管理的自動化。
到此這篇關(guān)于C++內(nèi)存管理詳細解析的文章就介紹到這了,更多相關(guān)C++
內(nèi)存管理內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://www.cnblogs.com/MiaoMiaoGarden/p/15579610.html