1. 程式人生 > >深入理解“堆”及物件本質

深入理解“堆”及物件本質

「深入理解」系列,本文是《深入理解“棧”及函式呼叫》的姊妹篇

首先宣告,本文談論的“堆”是指“堆記憶體”而不是“作為資料結構的堆”

“堆”的哲學:從無序中發現有序

類比“棧”學習“堆”

在百度百科中,對“堆”是這樣解釋的:“……累積在一起的東西; 累積在一起;聚積在一起;量詞,用於成堆的物或成群的人……”,這樣的描述難免給人一種“堆就是雜亂”之類的錯覺——而實際上,特別是在電腦科學中我們今天要談論的“堆”,其實是有序的,只是其元素的堆砌方式因為較通常的堆疊方式結構不太一樣所以不太容易發覺;以下是作為計算機科學術語的”堆“的較為準確描述:
堆記憶體是區別於棧區、全域性資料區和程式碼區的另一個記憶體區域;堆允許程式在執行時動態地申請某個大小的記憶體空間


如圖所示:
這裡寫圖片描述
這張圖很好的描述了“棧”和“堆”之間微妙的關係

堆的特點

即然我們要研究“堆”,就要先看看它都有哪些特點:
1. 記憶體分配取決於程式設計師,一些較為低階的語言(比如C、C++)還可以使用運算子手動釋放該片記憶體;
2. 和“作為資料結構的堆”是完全兩碼事,在結構上“堆記憶體”更像“連結串列”;
3. 所有的物件包括陣列物件都儲存在“堆”上;
4. “堆記憶體”被所有執行緒共享

儲存在堆上的物件

話不多說,直接上圖:
這裡寫圖片描述
看圖說話,可以看出“堆”上的物件是直接被棧中的控制代碼(引用)管理著的,就是這樣,堆負責產生真實的物件而棧負責管理物件

堆的訪問效率不如棧

讓我們深入硬體層面,看看堆是怎樣被使用的

對比圖:
這裡寫圖片描述
這裡寫圖片描述
解釋:訪問棧中的元素(方法、區域性變數)直接從地址讀取資料到暫存器,然後放到目標地址;而訪問堆中的元素(物件本體)先將分配的地址放到暫存器,然後取再出這個地址的值,最後放到目標地址,因為這要經過更多的資料傳送所以堆記憶體的開銷比較大;
另外,棧更快因為所有的空閒記憶體都是連續的,因此不需要對空閒記憶體塊通過列表來維護;只是一個簡單的指向當前棧頂的指標——編譯器通常用一個專門的、快速的暫存器來實現;更重要的一點是,隨後的棧上操作通常集中在一個記憶體塊的附近,這樣的話有利於處理器的高速訪問
舉一個具體的栗子:

char str
[] = "hello"; char *str = "hello";

這裡寫圖片描述
對應的彙編程式碼:
這裡寫圖片描述
小結:從棧中釋放“塊”只是簡單的指標偏移,但是堆和棧不一樣,從堆上分配和重新分配塊沒有固定模式;可以在任何時候分配和釋放它。這樣使得跟蹤哪部分堆已經被分配和被釋放變的異常複雜;有許多定製的堆分配策略用來為不同的使用模式下調整堆的效能

“堆”和垃圾回收

垃圾的產生和GC機制

在這之前,要先明確“動態記憶體分配”和“垃圾收集機制”兩個不同的概念,前者是指“由程式設計師動態管理(尤指釋放)之前分配的堆記憶體,而後者是坐享其成讓GC演算法代為回收垃圾記憶體”
典型的“動態記憶體分配”如“C(free)、C++(delete)”;典型的“垃圾收集機制”如“Java的GC機制”,這裡我們以Java的GC演算法為例講解垃圾回收機制

Java雖然為我們封裝了記憶體管理的細節,我們不需要手動delete掉之前new的那塊記憶體(因為JVM幫我們完成,對就是GC機制所謂的回收物件實際上是回收該物件所在的記憶體空間),但是在GC所用之前“垃圾”是存在的——通常判斷一個“物件”(實際是該物件所佔有的記憶體空間)是否是“垃圾”的判據可以有多種演算法,這裡提兩點:
1. 引用計數演算法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的物件就是不可能再被使用的;引用計數法又一個致命的缺點就是不能解決“迴圈引用”的問題(Java沒有使用這種演算法)
2. 可達性分析演算法:通過一系列的名為”GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連,即從GC Roots到這個物件不可達,則證明此物件是不可用的,Java的GC機制用的就是這種演算法

JVM是一個統稱,而本文的JVM特指“HotSpot JVM”

GC演算法在帶來便利的同時也拖慢了程式的執行速度

“堆”和記憶體碎片

記憶體碎片的概念

採用分割槽式儲存管理的系統,在儲存分配過程中產生的、不能供使用者作業使用的主存裡的小分割槽稱成“記憶體碎片”;記憶體碎片分為內部碎片和外部碎片
其中,“內部碎片”是由於計算機記憶體空間的“原子性”(最小位元組數不可分)造成的,在分配記憶體空間時的起始地址必須被4整除,如果當程式設計師malloc(C語言)一個14位元組的記憶體區域,OS會返回16位元組的記憶體空間;這樣就產生了一個空閒的2位元組空間,這部分空間就成為碎片;而“外部碎片”指的是頻繁的分配與回收物理頁面而導致大量的、連續且小的頁面塊夾雜在已分配的頁面中間,例如第一次分配0~9的記憶體空間,第二次分配10~14的記憶體空間,如果以後每次分配的記憶體空間都比原來的大,即時free掉最原來的0~9的記憶體空間也不夠用,因為記憶體分配的基礎是“陣列”,是連續的;總結起來造成記憶體碎片的原因主要分為兩點:1. 記憶體單位的“原子性”;2. 記憶體分配的“連續性”

這裡有必要說明一下,所謂的“記憶體碎片”和“垃圾物件”是不一樣的概念;雖然二者都是指一塊“不可用的記憶體空間”,但是“記憶體碎片沒有具體名稱,就是一塊遊離在有用記憶體間的幽靈,但是“垃圾物件”在”棧“空間中儲存著它的引用,只是因為它現在被指向了null(現在也沒有名字了)或者指向了別的物件;在某種意義上說,二者是同一種概念但是“碎片”更加的沒用

如何避免“碎片”

由“碎片”的產生原理可知,只要每次申請的記憶體都比之前的空間小就不會造成“碎片”;或者也可以考慮採用高效的資料結構比如“連結串列”,通過儲存相鄰元素的地址達到訪問下一個元素的目的,從而可以實現了記憶體區域的最大化利用
這裡寫圖片描述
這是“陣列式”資料儲存方式——連續的
這裡寫圖片描述
這是“連結串列式”資料存書方式——非連續的

通常陣列的儲存都是隱式的使用“陣列”為基本組織方式,我們往往根據需要採用不同的資料結構

完美完結!