内存那些事儿
目錄
一、C++內(nèi)存模型?
1.什么是內(nèi)存
2.C++內(nèi)存模型
(1)棧
(2)unused
(3)堆
(4)數(shù)據(jù)段
(5)代碼段(text)
二、各語言內(nèi)存模型
1.堆與棧的本質(zhì)是什么
2.各語言內(nèi)存模型
三、進(jìn)程與內(nèi)存
1.指針與引用
2.進(jìn)程獨(dú)占一個(gè)連續(xù)的4G大小的內(nèi)存
四、函數(shù)運(yùn)行時(shí)在內(nèi)存中是什么樣子???
1.函數(shù)運(yùn)行時(shí)棧 (run time stack)
2.棧幀(stack frames、call frames)
(1)棧幀
(2)控制轉(zhuǎn)移
(3)傳遞參數(shù)與獲取返回值
(4)局部變量
五、申請(qǐng)內(nèi)存時(shí)底層發(fā)生了什么??
1.內(nèi)核態(tài)&用戶態(tài)
(1)內(nèi)核區(qū)
(2)用戶區(qū)
2.標(biāo)準(zhǔn)庫
3.malloc內(nèi)存分配
六、自己實(shí)現(xiàn)malloc內(nèi)存分配器
1.為什么要?jiǎng)討B(tài)申請(qǐng)內(nèi)存?
2.內(nèi)存分配大致步
(1)跟蹤內(nèi)存分配狀態(tài)
(2)怎樣選擇空閑內(nèi)存塊
(3)內(nèi)存分配
(4)內(nèi)存釋放
七、內(nèi)存池技術(shù)是如何實(shí)現(xiàn)的??
1.內(nèi)存池技術(shù)
2.內(nèi)存池技術(shù)原理
3.線程局部存儲(chǔ)+內(nèi)存池
4.內(nèi)存池形式
?八、C++內(nèi)存管理全景指南
1.線程與數(shù)據(jù)競爭
2.?C++對(duì)象內(nèi)存模型
(1)空類
(2)非空類
(3)非空虛基類
3.常見解決內(nèi)存問題的方法
(1)靜態(tài)代碼檢測
(2)動(dòng)態(tài)代碼檢測
4.內(nèi)存池優(yōu)勢
九、C++成員函數(shù)在內(nèi)存中的存儲(chǔ)方式
十、C++ 虛函數(shù)表解析
1.虛函數(shù)表
2.虛函數(shù)&析構(gòu)函數(shù)
3.虛函數(shù)&繼承
(1)一般繼承(沒有overwrite)
(2)一般繼承(有虛函數(shù)覆蓋)
(3)多重繼承(無虛函數(shù)覆蓋)
(4)多重繼承(有虛函數(shù)覆蓋)
十一、為什么SSD不能當(dāng)做內(nèi)存用??
1.內(nèi)存條和固態(tài)硬盤的區(qū)別
(1)內(nèi)存條
(2)固態(tài)硬盤(SSD)
2.CPU是怎么訪問文件內(nèi)容的呢?
3.SSD壽命短
4.總結(jié)
十二、10 個(gè)內(nèi)存引發(fā)的大坑,你能躲開幾個(gè)???
1.malloc后要初始化
2.malloc后要free
一、C++內(nèi)存模型?
1.什么是內(nèi)存
????????內(nèi)存的基本單位:字節(jié)。一個(gè)字節(jié),8位:00000000
????????當(dāng)計(jì)算機(jī)在執(zhí)行我們的程序時(shí),無論是我們的機(jī)器指令還是機(jī)器指令操作的數(shù)據(jù),都需要存放在這些小盒子中(內(nèi)存)。
2.C++內(nèi)存模型
????????C/C++程序在存儲(chǔ)時(shí)有數(shù)據(jù)區(qū)和代碼區(qū)。
????????C/C++程序在被執(zhí)行時(shí),需要在內(nèi)存中劃出兩段區(qū)域用于存放數(shù)據(jù),這兩個(gè)區(qū)域就是我們熟悉的堆(Heap)和棧(Stack),也稱堆區(qū)和棧區(qū),其中堆區(qū)緊鄰數(shù)據(jù)段,在數(shù)據(jù)段之上,而棧在最上方,棧和堆之間是尚未被使用的內(nèi)存,隨著程序的運(yùn)行,當(dāng)程序申請(qǐng)內(nèi)存時(shí)棧區(qū)和堆區(qū)之間的空隙會(huì)減小,當(dāng)程序釋放內(nèi)存后空隙會(huì)擴(kuò)大,這就是C/C++程序的內(nèi)存模型。
?
參考文章:
C++內(nèi)存模型_五月525的博客-CSDN博客
C++內(nèi)存模型_五里的博客-CSDN博客_c++內(nèi)存模型
讓c/c++堆棧的工作機(jī)制變得通俗易懂?
(1)棧
????????進(jìn)行函數(shù)調(diào)用,保存參數(shù)、返回值及局部變量。
棧幀從高地址到低地址的方向依次是:
?
(2)unused
(3)堆
????????通過malloc或new動(dòng)態(tài)分配的內(nèi)存。
(4)數(shù)據(jù)段
????????在C中區(qū)分為bss段和data段(C++不區(qū)分):
- bss段是指那些沒有初始化的和初始化為0的全局變量。bss類型的全局變量只占運(yùn)行時(shí)的內(nèi)存空間,而不占文件空間。
- data段:初始化(非0)的全局變量和靜態(tài)變量。data類型的全局變量是即占文件空間,又占用運(yùn)行時(shí)內(nèi)存空間的。
(5)代碼段(text)
????????用來存放機(jī)器指令的一塊內(nèi)存區(qū)域。這部分區(qū)域的大小在程序運(yùn)行前就已經(jīng)確定,并且內(nèi)存區(qū)域通常屬于只讀,某些架構(gòu)也允許代碼段為可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。
????????text和data段都在可執(zhí)行文件中,由系統(tǒng)從可執(zhí)行文件中加載;而bss段不在可執(zhí)行文件中,由系統(tǒng)初始化。
????????不管數(shù)組在棧區(qū)還是堆區(qū),數(shù)組的生長方向都是從下往上的。也就是從低地址到高地址的。
????????棧的生長方向是由上往下(從高地址往低地址)。棧頂和棧底不是上下決定,而是由入棧方向決定!
????????堆的生長方向從下往上(從低地址到高地址)。
二、各語言內(nèi)存模型
1.堆與棧的本質(zhì)是什么
????????在編程語言中,堆區(qū)和棧區(qū)本質(zhì)上都是內(nèi)存,因此二者在本質(zhì)上沒有任何區(qū)別,只不過這兩塊內(nèi)存的使用方式是不一樣的。為什么要區(qū)分呢?根源在于:內(nèi)存是有限的。
????????在數(shù)據(jù)結(jié)構(gòu)與算法中,我們也有堆和棧的概念,但那里指的不是內(nèi)存,而是兩種數(shù)據(jù)結(jié)構(gòu)。
2.各語言內(nèi)存模型
????????C/C++分配內(nèi)存是直接在物理內(nèi)存中進(jìn)行的,而Java、Python等程序是將內(nèi)存分配請(qǐng)求交給解釋器,解釋器再去物理內(nèi)存上進(jìn)行分配。
????????Java、Python等程序的一大優(yōu)點(diǎn)就是內(nèi)存的自動(dòng)化管理,而C/C++程序員需要自己來管理從堆上分配的內(nèi)存。內(nèi)存管理這一項(xiàng)工作在Java、Python等程序中被解釋器接管了,解釋器的這項(xiàng)功能被稱為“垃圾回收器”。
三、進(jìn)程與內(nèi)存
1.指針與引用
????????只有C/C++這樣的編譯型語言才會(huì)有“指針”這樣一個(gè)概念,指的是當(dāng)前的對(duì)象放在了內(nèi)存中的哪個(gè)位置上了。在比如Java、Python等語言中只有“引用”這樣一個(gè)概念。
2.進(jìn)程獨(dú)占一個(gè)連續(xù)的4G大小的內(nèi)存
????????每個(gè)進(jìn)程獨(dú)占一個(gè)連續(xù)的4G大小的內(nèi)存,從內(nèi)存地址0開始,一直到0xffffffff,其中最上方的1G留給了操作系統(tǒng)使用,下方的3G是留給進(jìn)程自己使用的,其中程序員可以操作的區(qū)域就是圖中的堆區(qū)和棧區(qū)。
?
?????????每個(gè)進(jìn)程都認(rèn)為真實(shí)的內(nèi)存就是4G,其中1G被操作系統(tǒng)使用,剩余部分被進(jìn)程使用,也就是可以被程序員使用。注意這是不受真實(shí)物理內(nèi)存限制的,也就是說,即使真實(shí)的物理只有256MB,進(jìn)程同樣認(rèn)為在內(nèi)存是4G,其中1G是操作系統(tǒng)的,剩余3G是進(jìn)程自己獨(dú)占的,程序員依然可以按照內(nèi)存大小是3G來寫程序。所以在大小256MB的真實(shí)物理內(nèi)存上,程序員依然可以一次性申請(qǐng)超過256MB的內(nèi)存而且可以申請(qǐng)成功,后續(xù)內(nèi)存的使用也不受影響。
????????在虛擬內(nèi)存上程序員分配內(nèi)存不受真實(shí)物理內(nèi)存大小的限制。
四、函數(shù)運(yùn)行時(shí)在內(nèi)存中是什么樣子???
1.函數(shù)運(yùn)行時(shí)棧 (run time stack)
????????進(jìn)程和線程的運(yùn)行體現(xiàn)在函數(shù)執(zhí)行上,函數(shù)的執(zhí)行除了函數(shù)內(nèi)部執(zhí)行的順序執(zhí)行還有子函數(shù)調(diào)用的控制轉(zhuǎn)移以及子函數(shù)執(zhí)行完畢的返回。其中函數(shù)內(nèi)部的順序執(zhí)行乏善可陳,重點(diǎn)是函數(shù)的調(diào)用。
????????函數(shù)調(diào)用的活動(dòng)軌跡:是一個(gè)First In Last Out 的順序,天然適用于棧這種數(shù)據(jù)結(jié)構(gòu)來處理。即:使用棧這種結(jié)構(gòu)就可以用來保存函數(shù)調(diào)用信息。
2.棧幀(stack frames、call frames)
(1)棧幀
????????保存函數(shù)運(yùn)行時(shí)的各種信息。每個(gè)未完成運(yùn)行的函數(shù)占用一個(gè)獨(dú)立連續(xù)區(qū)域(包含這個(gè)函數(shù)涉及的參數(shù),局部變量,返回地址等相關(guān)信息),稱為棧幀。這些棧幀構(gòu)成了我們通常所說的棧區(qū)。
????????在計(jì)算機(jī)中,每個(gè)函數(shù)棧幀的“底部”和“頂部”的信息——也就是內(nèi)存地址,分別存放在兩個(gè)寄存器中:BasePointer(BP)寄存器以及StackPointer(SP)寄存器,即我們熟悉的rbp以及rsp,32位下為ebp以及esp。
????????當(dāng)調(diào)用函數(shù)時(shí),就要壓入一個(gè)新的棧幀,發(fā)起調(diào)用函數(shù)的棧幀成為調(diào)用者棧幀,被調(diào)用函數(shù)的棧幀則稱為當(dāng)前棧幀(rsp 和 rbp 之間的內(nèi)存空間);被調(diào)用的函數(shù)運(yùn)行結(jié)束后回收棧幀,回到調(diào)用者棧幀。這一過程都是自動(dòng)的,由系統(tǒng)分配與銷毀,無需手動(dòng)調(diào)度。
(2)控制轉(zhuǎn)移
????????A調(diào)用B:CPU從開始執(zhí)行屬于函數(shù)A的指令切換到執(zhí)行屬于函數(shù)B的指令,我們就說控制從函數(shù)A轉(zhuǎn)移到了函數(shù)B。
?
????????call指令除了給出跳轉(zhuǎn)地址之外還有這樣一個(gè)作用,也就是把call指令的下一條指令的地址,也就是0x40056a push到函數(shù)A的棧幀中。
call?? 首先將被調(diào)函數(shù)的參數(shù)入棧,最后是返回地址入棧,再跳到被調(diào)函數(shù)起始地址
leave? 準(zhǔn)備返回時(shí)的楨棧 : 令棧指針先指向當(dāng)前楨的起始處(這里保存的是調(diào)用者楨的起始地),出棧(楨指針重置為調(diào)用者楨的起始;且棧指針指向返回地址)
等同于 :? ?
????????movl %ebp,%esp?
????????popl %ebp
ret??? (棧指針指向返回地址)出棧并跳到那個(gè)位置(返回地址)。
(3)傳遞參數(shù)與獲取返回值
????????在x86-64中,多數(shù)情況下參數(shù)的傳遞與獲取返回值是通過寄存器來實(shí)現(xiàn)的。寄存器的數(shù)量是有限的,當(dāng)參數(shù)個(gè)數(shù)多于寄存器數(shù)量時(shí)剩下的參數(shù)直接放到棧幀中,這樣被調(diào)函數(shù)就可以從前一個(gè)函數(shù)的棧幀中獲取到參數(shù)了。
?
?????????調(diào)用函數(shù)B時(shí)有部分參數(shù)放到了函數(shù)A的棧幀中,同時(shí)函數(shù)A棧幀的頂部依然保存的是返回地址。
通常情況,棧都是從棧底往棧頂壓:
?
(4)局部變量
????????函數(shù)內(nèi)部定義的變量被稱為局部變量,這些變量同樣可以放在寄存器中,但是當(dāng)局部變量的數(shù)量超過寄存器的時(shí)候這些變量就必須放到棧幀中了。
?
?????????在向寄存器中寫入局部變量之前,一定要先將寄存器中開始的值保存起來,當(dāng)寄存器使用完畢后再恢復(fù)原值就可以了。那么我們要將寄存器中的原始值保存在哪里呢?沒錯(cuò),依然是函數(shù)的棧幀中。
?
?注意
????????棧區(qū)是有大小限制的,當(dāng)超過限制后就會(huì)出現(xiàn)著名的棧溢出問題,顯然上述代碼會(huì)導(dǎo)致這一問題的出現(xiàn)。因此:
五、申請(qǐng)內(nèi)存時(shí)底層發(fā)生了什么??
?
?????????x86 CPU提供了“四界”:0,1,2,3,這幾個(gè)數(shù)字其實(shí)就是指CPU的幾種工作狀態(tài),數(shù)字越小表示CPU的特權(quán)越大,0號(hào)狀態(tài)下CPU特權(quán)最大,可以執(zhí)行任何指令,數(shù)字越大表示CPU特權(quán)越小,3號(hào)狀態(tài)下CPU特權(quán)最小,不能執(zhí)行一些特權(quán)指令。
????????一般情況下系統(tǒng)只使用0和3,因此確切的說是“兩界”,這兩界指的是“用戶態(tài)(3)”以及“內(nèi)核態(tài)(0)”,接下來我們看看什么是內(nèi)核態(tài)、什么是用戶態(tài)。
1.內(nèi)核態(tài)&用戶態(tài)
(1)內(nèi)核區(qū)
????????CPU執(zhí)行操作系統(tǒng)代碼時(shí)就處于內(nèi)核態(tài),在內(nèi)核態(tài)下CPU可以執(zhí)行任何機(jī)器指令、訪問所有地址空間、不受限制的訪問任何硬件。
(2)用戶區(qū)
????????在用戶態(tài)我們的代碼處處受限,不能直接訪問硬件、不能訪問特定地址空間,否則操作系統(tǒng)直接將你kill掉,這就是著名的Segmentation fault、不能執(zhí)行特權(quán)指令。
????????操作系統(tǒng)為普通程序員留了一些特定的暗號(hào),這些暗號(hào)就和普通函數(shù)一樣,程序員通過調(diào)用這些暗號(hào)就能向操作系統(tǒng)請(qǐng)求服務(wù)了,這些像普通函數(shù)一樣的暗號(hào)就被稱為系統(tǒng)調(diào)用,System Call,通過系統(tǒng)調(diào)用我們可以讓操作系統(tǒng)代替我們完成一些事情,像打開文件、網(wǎng)絡(luò)通信等。
2.標(biāo)準(zhǔn)庫
????????標(biāo)準(zhǔn)庫對(duì)程序員屏蔽底層差異,這樣程序員寫的程序就無需修改的在不同操作系統(tǒng)上運(yùn)行了。從分層的角度看,我們的程序一般都是這樣的漢堡包類型:
?
?????????最上層是應(yīng)用程序,應(yīng)用程序一般只和標(biāo)準(zhǔn)庫打交道(當(dāng)然,我們也可以繞過標(biāo)準(zhǔn)庫),標(biāo)準(zhǔn)庫通過系統(tǒng)調(diào)用和操作系統(tǒng)交互,操作系統(tǒng)管理底層硬件。這就是為什么在C語言下同樣的open函數(shù)既能在Linux下打開文件也能在Windows下打開文件的原因。
3.malloc內(nèi)存分配
????????我們分配內(nèi)存時(shí)使用的malloc函數(shù)其實(shí)不是實(shí)現(xiàn)在操作系統(tǒng)里的,而是在標(biāo)準(zhǔn)庫中實(shí)現(xiàn)的。
????????平時(shí)在C語言中使用malloc只是內(nèi)存分配器的一種,實(shí)際上有很多內(nèi)存分配器,像tcmalloc,jemalloc等等,它們都有各自適用的場景,對(duì)于高性能程序來說使用滿足特定要求的內(nèi)存分配器是至關(guān)重要的。
????????如果內(nèi)存分配器中的空閑內(nèi)存塊不夠用了該怎么辦呢?malloc內(nèi)存不足時(shí)要向操作系統(tǒng)申請(qǐng)內(nèi)存,操作系統(tǒng)才是真大佬,malloc不過是小弟,對(duì)每個(gè)進(jìn)程,操作系統(tǒng)(類Unix系統(tǒng))都維護(hù)了一個(gè)叫做brk的變量,brk發(fā)音break,這個(gè)brk指向了堆區(qū)的頂部。brk()系統(tǒng)調(diào)用就是用來增加或者減小堆區(qū)的。
?
現(xiàn)在就可以簡單總結(jié)一下了,當(dāng)我們申請(qǐng)內(nèi)存時(shí),經(jīng)歷這樣幾個(gè)步驟:
?
?????????此時(shí)操作系統(tǒng)根本就沒有真正的分配物理內(nèi)存,程序員從malloc拿到的內(nèi)存目前還只是一張空頭支票。
????????當(dāng)我們真正使用這段內(nèi)存時(shí),這時(shí)會(huì)產(chǎn)生一個(gè)缺頁錯(cuò)誤,操作系統(tǒng)捕捉到該錯(cuò)誤后開始真正的分配物理內(nèi)存,操作系統(tǒng)處理完該錯(cuò)誤后我們的程序才能真正的讀寫這塊內(nèi)存。
所以,當(dāng)我們調(diào)用malloc申請(qǐng)內(nèi)存時(shí):
六、自己實(shí)現(xiàn)malloc內(nèi)存分配器
1.為什么要?jiǎng)討B(tài)申請(qǐng)內(nèi)存?
????????因?yàn)槲覀儾荒?strong>提前知道程序到底需要使用多少內(nèi)存。那我們什么時(shí)候才能知道呢?答案是只有當(dāng)程序真的運(yùn)行起來后我們才知道。如果能提前知道我們的程序到底需要多少內(nèi)存,那么直接知道告訴編譯器就好了,這樣也不必發(fā)明malloc等內(nèi)存分配器了。
????????每個(gè)進(jìn)程都認(rèn)為自己獨(dú)占內(nèi)存。
????????內(nèi)存動(dòng)態(tài)申請(qǐng)和釋放都發(fā)生在堆區(qū),heap。我們使用的malloc或者C++中的new申請(qǐng)內(nèi)存時(shí),就是從堆區(qū)這個(gè)區(qū)域中申請(qǐng)的。
2.內(nèi)存分配大致步
(1)跟蹤內(nèi)存分配狀態(tài)
????????將維護(hù)內(nèi)存塊的分配信息保存在內(nèi)存塊本身中;那么,為了維護(hù)內(nèi)存塊分配狀態(tài),我們需要知道哪些信息呢?很簡單:
- 一個(gè)標(biāo)記,用來標(biāo)識(shí)該內(nèi)存塊是否空閑
- 一個(gè)數(shù)字,用來記錄該內(nèi)存塊的大小
?
????????上圖中:每一方框代表4字節(jié)。紅色區(qū)域表示已經(jīng)分配出去的,灰色區(qū)域表示空閑內(nèi)存,每一塊內(nèi)存都有一個(gè)header,用帶斜線的方框表示,比如16/1,就表示該內(nèi)存塊大小是16字節(jié),1表示已經(jīng)分配出去了;而32/0表示該內(nèi)存塊大小是32字節(jié),0表示該內(nèi)存塊當(dāng)前空閑。通過每一個(gè)header的最后一個(gè)bit位就能知道每一塊內(nèi)存是空閑的還是已經(jīng)分配出去了,這樣我們就能追蹤到每一個(gè)內(nèi)存塊的分配信息。
????????最后一個(gè)方框0/1表示什么呢?原來,我們需要某種特殊標(biāo)記來告訴我們的內(nèi)存分配器是不是已經(jīng)到末尾了,這就是最后4字節(jié)的作用。
(2)怎樣選擇空閑內(nèi)存塊
| 分配策略 | ????????描述 | 優(yōu)缺點(diǎn) |
| first fit | 每次從頭開始找起,找到第一個(gè)滿足要求的就返回,這就是所謂的First fit方法,教科書中一般稱為首次適應(yīng)方法。 | 每次從頭開始找起,找到第一個(gè)滿足要求的就返回,這就是所謂的First fit方法,教科書中一般稱為首次適應(yīng)方法 |
| Next Fit | 別總是從頭開始找了,而是從上一次找到合適的空閑內(nèi)存塊的位置找起 | Next Fit將遠(yuǎn)快于First Fit |
| Best Fit | Best Fit算法會(huì)找到所有的空閑內(nèi)存塊,然后將所有滿足要求的并且大小為最小的那個(gè)空閑內(nèi)存塊返回,這樣的空閑內(nèi)存塊才是最Best的,因此被稱為Best Fit | 缺點(diǎn):分配內(nèi)存時(shí)需要遍歷堆上所有的空閑內(nèi)存塊,在速度上顯然不及前面兩種方法。 優(yōu)點(diǎn):更合理利用內(nèi)存 |
(3)內(nèi)存分配
????????如果空閑內(nèi)存塊大于所需內(nèi)存:將空閑內(nèi)存塊進(jìn)行劃分,前一部分設(shè)置為已分配,返回給內(nèi)存申請(qǐng)者使用,后一部分變?yōu)橐粋€(gè)新的空閑內(nèi)存塊,只不過大小會(huì)更小而已。
(4)內(nèi)存釋放
????????我們要考慮到的關(guān)鍵一點(diǎn)就在于,與被釋放的內(nèi)存塊相鄰的內(nèi)存塊可能也是空閑的。實(shí)際使用的內(nèi)存分配器都會(huì)有某種推遲合并空閑內(nèi)存塊的策略。
Q:合并內(nèi)存塊的時(shí)候,如何知道上一個(gè)內(nèi)存塊是不是空閑的?
A:我們不是有一個(gè)信息頭header嗎,那么我們就在該內(nèi)存塊的末尾再加一個(gè)信息尾,footer,footer一詞用的很形象,header和footer的內(nèi)容是一樣的。
????????因?yàn)樯弦粌?nèi)存塊的footer和下一個(gè)內(nèi)存塊的header是相鄰的,因此我們只需要在當(dāng)前內(nèi)存塊的位置向上移動(dòng)4直接就可以等到上一個(gè)內(nèi)存塊的信息,這樣當(dāng)我們釋放內(nèi)存時(shí)就可以快速的進(jìn)行相鄰空閑內(nèi)存塊的合并了。
七、內(nèi)存池技術(shù)是如何實(shí)現(xiàn)的??
????????malloc性能不高的原因一在于其沒有為特定場景做優(yōu)化,除此之外還在于malloc看似簡單,但是其調(diào)用過程是很復(fù)雜的,一次malloc的調(diào)用過程可能需要經(jīng)過操作系統(tǒng)的配合才能完成。既然每次分配內(nèi)存都要經(jīng)過這么復(fù)雜的過程,那么如果程序大量使用malloc申請(qǐng)內(nèi)存那么該程序注定無法獲得高性能。
????????幸好,除了大眾貨的malloc,我們還可以私人定制,也就是針對(duì)特定場景自己來維護(hù)內(nèi)存申請(qǐng)和分配,這就是高性能高并發(fā)必備的內(nèi)存池技術(shù)。
1.內(nèi)存池技術(shù)
????????那malloc和這里提到的內(nèi)存池技術(shù)有什么區(qū)別呢?
?
?2.內(nèi)存池技術(shù)原理
????????內(nèi)存池技術(shù)一次性獲取到大塊內(nèi)存,然后在其之上自己管理內(nèi)存的申請(qǐng)和釋放,這樣就繞過了標(biāo)準(zhǔn)庫以及操作系統(tǒng)。
3.線程局部存儲(chǔ)+內(nèi)存池
????????多線程使用內(nèi)存池,除了使用鎖以外,我們還有別的方案,如:我們?yōu)槊總€(gè)線程維護(hù)一個(gè)內(nèi)存池就好了,這樣多線程間就不存在競爭問題了。線程局部存儲(chǔ),Thread Local Storage正是用于解決這一類問題的。
4.內(nèi)存池形式
????????第一種是提前創(chuàng)建出一堆需要的對(duì)象(數(shù)據(jù)結(jié)構(gòu)),自己維護(hù)好哪些對(duì)象(數(shù)據(jù)結(jié)構(gòu))可用哪些已被分配;
????????第二種可以申請(qǐng)任意大小的內(nèi)存空間,使用過程中只申請(qǐng)不釋放,最后一次性釋放。前兩種內(nèi)存池天然適用于服務(wù)器端編程。
????????第三種可以提前申請(qǐng)出一大段內(nèi)存,然后將這一大段內(nèi)存切分為大小相同的小內(nèi)存塊,自己來維護(hù)(使用棧)這些被切分出來的小內(nèi)存塊哪些是空閑的哪些是已經(jīng)被分配的。
?八、C++內(nèi)存管理全景指南
1.線程與數(shù)據(jù)競爭
????????一個(gè)表達(dá)式的求值寫入內(nèi)存位置,而另一求值讀或?qū)懲粌?nèi)存位置時(shí),稱這些表達(dá)式?jīng)_突。擁有二個(gè)沖突求值的程序有數(shù)據(jù)競爭,除非
- 兩個(gè)求值都在同一線程上,或同一信號(hào)處理函數(shù)中執(zhí)行,或
- 兩個(gè)沖突求值都是原子操作(見?std::atomic?),或
- 一個(gè)沖突求值先發(fā)生于( happens-before )另一個(gè)(見內(nèi)存順序--std::memory_order?)
????????若出現(xiàn)數(shù)據(jù)競爭,則程序的行為未定義。
2.?C++對(duì)象內(nèi)存模型
????????VS中:先選擇左側(cè)的C/C++->命令行,然后在其他選項(xiàng)這里寫上/d1 reportAllClassLayout,它可以看到所有相關(guān)類的內(nèi)存布局,如果寫上/d1 reportSingleClassLayoutXXX(XXX為類名),則只會(huì)打出指定類XXX的內(nèi)存布局。近期的VS版本都支持這樣配置。
(1)空類
class A { };?????????sizeof(A)?=?1,C++標(biāo)準(zhǔn)要求C++的對(duì)象大小不能為0,C++對(duì)象必須在內(nèi)存里面有唯一的地址,但又不想浪費(fèi)太多內(nèi)存空間,所以標(biāo)準(zhǔn)規(guī)定為1byte。
(2)非空類
class A { public:int a; };????????sizeof(A) = 4。
(3)非空虛基類
????????在普通類里面,如果有虛函數(shù)的話就會(huì)在最開始的地方添加一個(gè)隱藏的成員變量,虛函數(shù)表指針,然后才到正常的成員變量。
class A { public:int a=10;virtual void v(); };????????sizeof(A) = 16。
3.常見解決內(nèi)存問題的方法
(1)靜態(tài)代碼檢測
- 自動(dòng)執(zhí)行靜態(tài)代碼分析,快速定位代碼隱藏錯(cuò)誤和缺陷。
- 幫助代碼設(shè)計(jì)人員更專注于分析和解決代碼設(shè)計(jì)缺陷。
- 減少在代碼人工檢查上花費(fèi)的時(shí)間,提高軟件可靠性并節(jié)省開發(fā)成本。
一些主流的靜態(tài)代碼檢測工具:
- ?免費(fèi)的cppcheck,clang static analyzer;
- 商用的coverity,pclint等?
(2)動(dòng)態(tài)代碼檢測
4.內(nèi)存池優(yōu)勢
????????new/delete會(huì)造成碎片化,推薦使用內(nèi)存池。
????????內(nèi)存池方案通常一次從系統(tǒng)申請(qǐng)一大塊內(nèi)存塊,然后基于在這塊內(nèi)存塊可以進(jìn)行不同內(nèi)存策略實(shí)現(xiàn),一般采用內(nèi)存池有以下好處:
?????? 1.少量系統(tǒng)申請(qǐng)次數(shù),非常少(幾沒有) 堆碎片。
?????? 2.由于沒有系統(tǒng)調(diào)用等,比通常的內(nèi)存申請(qǐng)/釋放(比如通過malloc, new等)的方式快。
?????? 3.可以檢查應(yīng)用的任何一塊內(nèi)存是否在內(nèi)存池里。
?????? 4.寫一個(gè)”堆轉(zhuǎn)儲(chǔ)(Heap-Dump)”到你的硬盤(對(duì)事后的調(diào)試非常有用)。
?????? 5.可以更方便實(shí)現(xiàn)某種內(nèi)存泄漏檢測(memory-leak detection)。
?????? 6.減少額外系統(tǒng)內(nèi)存管理開銷,可以節(jié)約內(nèi)存。
九、C++成員函數(shù)在內(nèi)存中的存儲(chǔ)方式
class A { public: void printA() { cout<<"printA"<<endl; } virtual void printB() { cout<<"printB"<<endl; } }; int main(void) {A *a=NULL;a->printA();a->printB(); }? ? ? ? 輸出“printA”后,程序崩潰。
????????個(gè)人理解:類的非靜態(tài)類成員函數(shù)其實(shí)都內(nèi)含了一個(gè)指向類對(duì)象的指針型參數(shù)(即this指針),對(duì)于普通函數(shù),d->printA(this),printA中沒有使用成員變量,所以不會(huì)崩潰。而對(duì)于虛函數(shù),存在一個(gè)vptr指向虛函數(shù)表,但是this不存在,所以this->vptr會(huì)崩潰。
????????類的非靜態(tài)類成員函數(shù)其實(shí)都內(nèi)含了一個(gè)指向類對(duì)象的指針型參數(shù)(即this指針),因而只有類對(duì)象才能調(diào)用(此時(shí)this指針有實(shí)值)。
????????new出來的只是成員變量,成員函數(shù)始終存在,所以如果成員函數(shù)未使用任何成員變量的話,不管是不是static的,都能正常工作。
????????含有虛函數(shù)的類中會(huì)有一個(gè)虛函數(shù)指針,本身占用一定空間,虛函數(shù)指針是通過查找虛函數(shù)表確定調(diào)用函數(shù)的,如果同時(shí)new多個(gè)對(duì)象,虛函數(shù)表可以共用。
????????如果一個(gè)類包括了數(shù)據(jù)和函數(shù),那么我們會(huì)以為定義對(duì)象時(shí)要分別為數(shù)據(jù)和函數(shù)的代碼分配存儲(chǔ)空間。這就錯(cuò)了,C++編譯系統(tǒng)下,每個(gè)對(duì)象所占用的存儲(chǔ)空間只是該對(duì)象的數(shù)據(jù)部分(虛函數(shù)指針和虛基類指針也屬于數(shù)據(jù)部分)所占用的存儲(chǔ)空間,而不包括函數(shù)代碼所占用的存儲(chǔ)空間。
十、C++ 虛函數(shù)表解析
1.虛函數(shù)表
????????虛函數(shù)表在編譯的時(shí)候就確定了,而類對(duì)象的虛函數(shù)指針vptr是在運(yùn)行階段確定的,這是實(shí)現(xiàn)多態(tài)的關(guān)鍵!
????????程序構(gòu)建(Build)的四個(gè)過程(預(yù)編譯、編譯、匯編和鏈接),推導(dǎo)出虛函數(shù)表應(yīng)該是在編譯期確定的,原因如下:
1)預(yù)編譯期主要處理那些源代碼文件中的以“#”開始的預(yù)編譯指令,如“#include”、“#define”。很明顯這個(gè)過程可以排除。
2)編譯期要做的事情就比較多了,包括詞法分析、語法分析、語義分析及優(yōu)化代碼等,是整個(gè)程序構(gòu)建的核心。所以,排除了預(yù)編譯期、匯編期、鏈接期及考慮到編譯期所做的事情,虛函數(shù)表應(yīng)該是在編譯期建立的。靜態(tài)庫在程序編譯時(shí)會(huì)被連接到目標(biāo)代碼中。
3)匯編期是將編譯器生成的匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語句幾乎都對(duì)應(yīng)一條機(jī)器指令。匯編過程相對(duì)于編譯期來說比較簡單,沒有復(fù)雜的語法,也沒有語義,也不需要做指令優(yōu)化,只是根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就行了。所以,匯編期也是可以排除的。
4)鏈接期(現(xiàn)只考慮靜態(tài)鏈接)是將匯編器生成的目標(biāo)文件(和庫)鏈接成一個(gè)可執(zhí)行文件,本質(zhì)上做的是重定位(Relocation)的工作。很明顯鏈接期也是可以排除的。
????????虛函數(shù)(Virtual Function)是通過一張?zhí)摵瘮?shù)表(Virtual Table)來實(shí)現(xiàn)的,簡稱為V-Table。在這個(gè)表中,主要是一個(gè)類的虛函數(shù)的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實(shí)反應(yīng)實(shí)際的函數(shù)。這樣,在有虛函數(shù)的類的實(shí)例中這個(gè)表被分配在了這個(gè)實(shí)例的內(nèi)存中,所以,當(dāng)我們用父類的指針來操作一個(gè)子類的時(shí)候,這張?zhí)摵瘮?shù)表就顯得由為重要了,它就像一個(gè)地圖一樣,指明了實(shí)際所應(yīng)該調(diào)用的函數(shù)。
????????C++的編譯器應(yīng)該是保證虛函數(shù)表的指針存在于對(duì)象實(shí)例中最前面的位置(這是為了保證取到虛函數(shù)表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。?這意味著我們通過對(duì)象實(shí)例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。
2.虛函數(shù)&析構(gòu)函數(shù)
? ??????????為什么析構(gòu)函數(shù)必須是虛函數(shù),而C++默認(rèn)的析構(gòu)函數(shù)不是虛函數(shù)?
????????將可能會(huì)被繼承的父類的析構(gòu)函數(shù)設(shè)置為虛函數(shù),可以保證當(dāng)我們new一個(gè)子類,然后使用基類指針指向該子類對(duì)象,釋放基類指針時(shí)可以釋放掉子類的空間,防止內(nèi)存泄漏。
????????C++默認(rèn)的析構(gòu)函數(shù)不是虛函數(shù)是因?yàn)樘摵瘮?shù)需要額外的虛函數(shù)表和虛表指針,占用額外內(nèi)存。而對(duì)于不會(huì)被繼承的類來說,就會(huì)浪費(fèi)內(nèi)存。因此C++默認(rèn)的析構(gòu)函數(shù)不是虛函數(shù),而是只有當(dāng)需要當(dāng)作父類時(shí),設(shè)置為虛函數(shù)。
3.虛函數(shù)&繼承
(1)一般繼承(沒有overwrite)
? ? ? ? ? ?????
(2)一般繼承(有虛函數(shù)覆蓋)
??????????????
(3)多重繼承(無虛函數(shù)覆蓋)
? ? ? ???
(4)多重繼承(有虛函數(shù)覆蓋)
????
十一、為什么SSD不能當(dāng)做內(nèi)存用??
1.內(nèi)存條和固態(tài)硬盤的區(qū)別
(1)內(nèi)存條
????????主要提供數(shù)據(jù)中轉(zhuǎn)的功能。打開軟件后,因?yàn)镃PU直接讀取固態(tài)硬盤上的數(shù)據(jù)非常慢,CPU會(huì)先將要用到的數(shù)據(jù)從固態(tài)遷移到內(nèi)存,內(nèi)存條的運(yùn)行速度更快,進(jìn)而提高運(yùn)行效率。關(guān)機(jī)后內(nèi)存條上的數(shù)據(jù)無法保存。
(2)固態(tài)硬盤(SSD)
????????是用來真正存儲(chǔ)數(shù)據(jù)的地方,俗稱的“C盤”、“D盤”……都是來自固態(tài)硬盤。
????????固態(tài)硬盤是增加系統(tǒng)和軟件的運(yùn)行速度,比如打開一個(gè)軟件就要快很多。內(nèi)存是增加電腦多任務(wù)處理的能力,比如你玩游戲的時(shí)候聽歌或者聊天,內(nèi)存小的電腦處理起來會(huì)有些卡頓。
2.CPU是怎么訪問文件內(nèi)容的呢?
????????文件系統(tǒng)把SSD上的數(shù)據(jù)以文件的形式呈現(xiàn)出來,程序直接操作文件,讀寫文件時(shí)把請(qǐng)求發(fā)送給文件系統(tǒng),文件系統(tǒng)把請(qǐng)求路由給SSD,SSD處理完請(qǐng)求后數(shù)據(jù)會(huì)被copy到相應(yīng)進(jìn)程的內(nèi)存中,此后程序直接操作內(nèi)存。
????????CPU無法直接按照字節(jié)粒度去訪問SSD,因此CPU無法脫離內(nèi)存直接在SSD中運(yùn)行你寫的程序。
3.SSD壽命短
????????SSD的制造原理決定了這類存儲(chǔ)設(shè)備是有固定使用壽命的。SSD有TBW(Max Terabytes Written,總寫入字節(jié))這個(gè)限制的,內(nèi)存則沒有這個(gè)問題。
????????因此如果你把SSD當(dāng)內(nèi)存用的話,相信很快你的SSD就會(huì)被CPU寫死。
4.總結(jié)
????????我們還沒有辦法直接把SSD當(dāng)做內(nèi)存來用,我們的各種軟件包括操作系統(tǒng)、文件系統(tǒng)以及各種硬件包括CPU等都沒有做好把SSD當(dāng)做內(nèi)存來用的準(zhǔn)備。
????????更新補(bǔ)充:Intel推出的傲騰持久內(nèi)存,Intel Optane Persistent Memory ,這種硬件可以當(dāng)做內(nèi)存來用,和普通內(nèi)存一樣,但同時(shí)也具有非易失特性,像磁盤或者SSD一樣斷電后內(nèi)存中的數(shù)據(jù)不會(huì)丟失。
十二、10 個(gè)內(nèi)存引發(fā)的大坑,你能躲開幾個(gè)???
1.malloc后要初始化
當(dāng)調(diào)用 malloc 時(shí)實(shí)際上有以下兩種可能:
2.malloc后要free
int* p = (int*)malloc(sizeof(int)*1024*1024);//4Mmemset(p, 0, sizeof(int) * 1024 * 1024);std::cout << *p << std::endl;free(p);總結(jié)
- 上一篇: CAD常见的20个问答
- 下一篇: amazon - amzreport 之