內存是怎樣一步步被分配出來的?
大家好,我是小風哥,今天簡單聊聊內存分配。
我們申請一塊內存時計算機內部發生了什么?看下這句代碼:
圖片
這里有兩部分,一個是malloc,再一個是你寫的代碼。
圖片
malloc實際上屬于標準庫,標準庫里有什么呢?
數學相關的函數,sin、cos、絕對值、數冪函數等;字符相關函數,判斷大小寫等;字符串操作函數、字符串拷貝、拼接比較等;當然還有內存管理函數,就是這里提到的malloc/free,當然還有很多其它函數,這就是標準庫。
圖片
再來看你寫的代碼,什么是你寫的代碼呢?以c語言為例,.c文件就是你寫的代碼,這包括你寫的hello world程序、充滿bug的練習程序,當然還有各種項目。
圖片
這就是你寫的代碼。
這些代碼怎么變成最終的可執行程序呢?當然是借助編譯器。
編譯器會把你的代碼編譯成目標文件。
圖片
接著鏈接器出場,連接器會把目標文件和標準庫打包成可執行程序。
圖片
這就是代碼部分,接下來我們看內存分配。
到底什么是內存呢?
圖片
內存實際上和儲物柜非常相似,儲物柜會劃分成了一個一個大小相同的隔間,每個隔間可以存儲東西,內存的道理也一樣,內存也被劃分成了一個一個大小相同的隔間,我們來仔細看一下。
圖片
內存中的每個隔間存儲的是一個字節,8比特位一字節。
比如這里申請的一塊int大小的內存,一個int占據4個字節。
圖片
和儲物柜一樣,內存中的每個隔間也有一個編號,這個編號叫做內存地址。
在我們的實例中,申請的這塊內存位于內存地址2這個位置,這意味著什么嗯?這意味著變量p等于數字2,或者說等于內存地址2,這里的p就是所謂的指針。
圖片
接著我們看內存分配過程。
這段代碼當然屬于編譯后生成的可執行程序,可執行程序是在內存中運行的,當然我們需要為整個程序分配一塊內存。
圖片
程序的運行依賴棧區,這里存放著局部變量等信息;依賴堆區,這里存放著程序員自己管理的動態申請的內存,關于堆區和棧區之前的視頻也有講解;除此之外還依賴代碼區,這里保存的就是編譯后的之類;還有數據區,這里保存著全局變量等信息。
圖片
這些區域在內存中的布局是這樣的:
圖片
再次強調下,編譯后的代碼位于代碼區,malloc動態申請的內存位于堆區,接下來我們只關注堆區。
在程序開始運行時堆區當然是空的,那么所謂的內存分配到底是什么呢?如果讓你實現內存分配器該怎么做到呢?很簡單,其實內存分配就是劃分地盤。
圖片
此時要分配第一塊大小為A的內存,那么你應該把A放在哪里呢?
圖片
因為此時堆區是空的,顯然你可以把開始這個位置劃分給A,作為A的地盤,找到A的地盤后malloc這個函數返回,內存分配過程結束,是不是很簡單。
圖片
接著程序員又開始申請大小為B的內存,道理和A一樣,把A之后的地盤給B即可。
圖片
程序員又開始申請大小為C的內存,同理。
圖片
接著程序員說A占用的這塊內存使用完畢,調用free釋放,所謂釋放就是把A占據的地盤重新標記為空閑,這時堆區里還有兩塊空閑內存。
圖片
接著程序員開始申請大小為D,這時問題來了,你該從哪里給D劃分地盤呢?
圖片
放到第一個空閑塊嗎?顯然第一個空閑塊大小不夠。
圖片
第二個呢,第二個也不夠。
圖片
但是你發現了一個問題,仔細看著兩個空閑塊,這兩個空閑塊的總大小實際上是超過D的。
圖片
我們把這種空閑的但是不能用來分配出去的內存稱之為內存碎片。
圖片
你可以想象一下經過不斷的內存申請和釋放,堆區中會存在無數這樣空閑內存碎片。
圖片
碎片化的內存顯然不利于內存的充分利用,計算機科學歷史上有無數論文試圖來解決這個問題。
現在堆區已經不足以為D申請出內存,該怎么辦呢?
讓我們回到最初的布局,注意看堆區和棧區中間實際上還有一段空閑內存區域,這塊區域就是為堆區或棧區來擴大地盤用的,那么該怎么擴大堆區呢?
圖片
這就要借助操作系統的幫助了。
在linux等系統中可以借助brk等系統調用向操作系統申請來擴大堆區。
圖片
現在堆區擴容完畢,此時就可以在堆區中找出一塊合適的空閑內存分配給D,到這時malloc這個過程才真正結束,這實際上是一個相當復雜的過程。