漫談 C++ 的 內存堆 實現原理

若是我來設計 C++ 的 內存堆 , 我會這樣設計 :                     html

 

進程 首先會跟 操做系統 要 一塊大內存區域 , 我稱之爲 Division , 簡稱 div 。 算法

而後 , 將這塊 div 做爲 堆 , 就能夠開始 從堆裏分配 內存 了 。數據庫

 

堆裏 未分配 可以使用 的 內存區域 稱之爲 Free Space , 一開始的時候 , div 裏 只有一個 Free Space , 就是 整個 div 。編程

若是 只分配 不回收 的話 , div 裏 永遠都只有一個 Free Space 。 隨着 分配 和 回收 , div 裏會產生多個 Free Space 。安全

咱們須要創建一張 堆表 來 記錄 Free Space , 這樣才能知道 每一次分配 應該 到 哪一個 Free Space 裏 分配 。網絡

 

堆表 應該是一個 鏈表 , 便於 插入 和 刪除 表項 。 表項 就是 Free Space , 或者說 表項 描述 Free Space 。 因此 表項 會包含 2 個 字段 , 一個是 Free Space 的 起始地址 , 另外一個是 Free Space 的 結束地址 。數據結構

同時 還應該有一個 指針 , 指向 當前在用的 表項 , 一次分配 就是 在 當前表項 指向的 Free Space 裏分配 , 若是 當前 Free Space 的 大小 不足以分配本次申請的 內存塊大小 , 則 將指針 指向 當前 Free Space 的 下一個 Free Space 。 若是 下一個 Free Space 的 大小也不夠 , 那麼 就繼續指向 下一個 Free Space 。 如此循環 。併發

 

那若是 最後一個 Free Space 的大小也不夠的話 , 就須要向 操做系統 要 一個 新的 div 。 注意 , Free Space 只能屬於一個 div , 不能跨 div 。app

 

若是 堆裏的 Free Space 比較多 , 那麼 若是 Free Space 大小不夠 , 有可能會連續找多個 Free Space 才找到 足夠大小的 Free Space , 這裏就產生了一個 性能問題 。高併發

最壞的狀況 , 「從頭找到尾」 , 到最後一個 Free Space 才足夠大小 。 但 , 這還不是最壞的 ^^ , 若是最後一個 Free Space 的大小也不夠的話 , 就要跟操做系統要一個 新的 div , 這好像要 「更壞」 一點 。  ^^

 

還有一個重要的問題須要考慮 , 就是 若是 跟操做系統要了 1 個以上的 div , 若是長期佔用 , 這是一個不小的空間 。 那麼 , 要怎樣在 div 中的內存所有都已經回收 (整個 div 是一個 Free Space) 的時候 , 將 div 歸還操做系統呢 ?  

 

能夠經過一個 計數器 。 能夠爲每一個 div 設置一個 計數器 , 同時在 堆表項 裏增長一個 字段 : Free Space 所在的 div 。

這樣 , 每次 分配 的時候 就在 計數器 裏 加 1 , 每次 回收 就讓 計數器 減 1 , 若是 減 1 之後 計數器 的 值 是 0 , 那麼就說明 div 已經所有回收 , 能夠將 div 歸還 操做系統 。

 

最後 , 我很好奇 , C++ 是怎麼解決 內存碎片 的問題的 。 哈哈哈哈

 

忽然發現 堆 的 管理算法 有點 小複雜 , 若是 堆表 自己佔用的內存空間是 固定 的 , 那麼若是 Free Space 的數量超出了 對錶 的空間所能存儲的數量 , 這就有問題 , 若是捨棄一些 比較小的 Free Space , 會形成 內存泄露 。

若是 堆表 的存儲空間也是經過 堆 的方式來分配 , 那麼 , 當應用程序申請了一塊內存 , 此時產生了一個 新的 Free Space , 爲了記錄這個 Free Space , 須要爲描述這個 Free Space 的 堆表項 也 申請一塊內存 , 這樣 Free Space 又會發生變化 , 可能產生 1 個新的 Free Space, 或者 要記錄的這個 Free Space 發生變化 , 須要把這些狀況也考慮進去 。

還有一種狀況是 歸還 內存塊 的時候 , 這個內存塊恰好在 2 個 Free Space 中間 , 那麼歸還這個內存塊就不是簡單的在 堆表 裏添加一個 堆表項 , 而是要和 先後 2 個 FreeSpace 「合併」 起來 。 這 3 個 Free Space 會 合併成 1 個 Free Space , 在 堆表 裏 會 刪除 原來的 2 個 Free Space 表項 , 同時在 這 2 個 表項 的位置 添加入 合併後的 新表項 。

問題是 , 要怎麼知道 歸還的內存塊 在 某 2 個 Free Space 中間 ? 好像只能 遍歷 。 但這意味着 每次 歸還的時候都要 遍歷 。

而後 。

實際上 , 不只僅 內存塊 在 2 個 Free Space 之間會存在這個問題 , 只要 歸還的內存塊 的 任一邊(前 或 後) 和 1 個 Free Space 相連 , 都須要 「合併」 。

若是要快速的找到 和 本身鄰近的 Free Space , 可能須要創建 索引 。 能夠創建 不止一個 的 索引 。

好比 能夠 按 起始位置 創建索引 , 同時還能夠按 Free Space 的大小 創建索引 。 前者能夠快速的尋找 和當前 歸還的內存塊 相鄰的 Free Space 。 後者能夠快速的尋找接近指定大小的 Free Space , 這能夠用在 分配 的 時候 , 尋找接近 申請內存塊大小 的 Free Space 進行分配 有利於提升 內存利用率 , 減小碎片 。

索引 也能夠排序 , 若是要 優先從 小的 Free Space 或者 大的 Free Space 來 分配 的話 , 索引的 排序做用 也能夠派上用場 。

關於索引 , 我在 《我發起了一個 .Net 開源數據庫項目 SqlNet》 http://www.javashuo.com/article/p-gpuysmcv-q.html    中有一些論述 。 實際上 , 我正是考慮 數據庫 中 Data Block 的 Free Space 如何管理 , 因此才繼續思考 內存堆 的 管理問題 , 而後就產生了上面的一些思考結果 。

 

能夠設想一下具體的作法 :

若是不考慮 堆 的 無限增加 的話 , 設計起來並不太難 :)  所謂 無限增加 , 主要是指 堆表 的無限增加 。 堆表 爲何會無限增加呢 ? 堆表 是保存 Free Space 的 , 若是 Free Space 無限增加 , 那麼 堆表 就會無限增加 。 Free Space 的數量是不肯定的 , 但理論上 , 彷佛不能給出一個限制 。 若是咱們給定 堆表 的長度是 1萬 , 那麼就只能記錄 1萬 個 Free Space , 超出 1萬 個的 Free Space 會由於不能記錄而處於 「遺棄」 的狀態 , 既不能 分配 也不能回收 。 這就形成了 內存泄漏 。

若是在 堆表 達到上限的時候 拋出 異常 「堆表超出最大範圍」 , 就像 StackOverflow 或者 OutOfMemory , 但這可能會限制了應用程序的能力 。

若是按照上文的說法 , 堆表 的 存儲 自己也徹底經過 堆分配 進行 , 這樣能夠很靈活 , 看起來只要內存空間足夠 , 那麼 , 堆表 能夠無限增加 。

但這種作法 是 「本身描述本身」 的一個 循環 , 會致使算法複雜 , 循環 , 或者 無解 。 因此咱們放棄了這種方式 。

問題出在哪裏呢 ? 堆表項 自身對於 內存空間的 佔用不能 計算到 堆 的分配裏 。 堆表應該是單獨佔用一塊空間 , 堆表項 及 索引項 的 添加刪除 在這個空間也會形成 空閒空間 (Free Space) , 但這些 Free Space 不能 計算到 堆 裏 , 而應該是 獨立 於 堆 的 存在 。 不然就會陷入上述的 「本身描述本身」 的 循環 。 總之狀況很複雜 , 可能無解 。 固然也許有解 , 但我不想繼續思考下去了  :)  

因此 , 回到開始 , 若是不考慮 堆 的 無限增加 的話 , 就是說 給定一個 堆表 的 固定大小 , 咱們這樣來設計 堆 試試看 。 通過上面的論述 , 實際上 , 若是要設計 無限增加的 堆表 , 那麼 , 在 固定大小 的 堆表 基礎上 , 增長一點 : 當 當前堆表 空間不夠時 , 再申請一塊 堆表空間 用於 繼續存放 堆表 , 這樣 堆表 就能繼續增加了  。 

 

咱們提供一塊 連續的 內存空間 來 存儲 堆表 , 這塊 內存空間 咱們 稱之爲 堆表空間 。 按照上面說的 , 咱們先嚐試實現 一個固定大小 的 堆表空間 的 堆 。

堆表 的內容 包括 Free Space 項 和 索引 。 索引 由 索引項 組成 , 索引項 最終會指向 堆表項 , Free Space 項 之間經過 鏈表 的方式 相連 。 Free Space 項 和 索引項 都 存儲在 堆表空間 裏  。

堆表 還 包括一個 指針 , 指向 堆表 的最後一個元素的結束地址的下一個地址 , 咱們將這個指針 稱爲 「Append 指針」 。

全部 新建 的 堆表項(Free Space 項 和 索引項) 都 添加至 Append 指針 指示的 地址 , 每添加完一個 堆表項 , Append 指針 會指向這個 堆表項 的 結束地址 的 下一個地址 。 當 Append 指針指向的 地址 到 堆表 的結束地址 之間的空間 不夠 存放新的 堆表項 時 , 會檢查 「堆表空閒空間計數器」 , ——   等   ——     什麼是 「堆表空閒空間計數器」 ? 在 堆表 的使用過程當中 , 隨着 Free Space 項 和 索引項 的 添加 刪除 , 固然也會出現 「空閒空間」 , 咱們會用一個 整數變量 , 來記錄空閒空間有多少(以 Byte 爲單位) , 每次刪除 堆表項 (Free Space 項 和 索引項) 的時候 , 會 將 回收 的 空閒空間 累計 到 這個 整數變量 裏 。 這個變量 就是 「堆表空閒空間計數器」 。 注意 , 「堆表空閒空間計數器」 記錄的是 Append 指針指向的地址以前 「已使用的空間」 中 因 堆表項 的 刪除 而 「空出來」 的 空閒空間 。 這些 空閒空間 平時不會去動它 , 只有上面說的 「當 Append 指針指向的 地址 到 堆表 的結束地址 之間的空間 不夠 存放新的 堆表項 時」 , 纔會去關心 它 。 怎麼關心呢 ? 這個時候 , 會作一次 「垃圾回收」 , 就是把 這些 空閒空間 後面 的數據 向前移動 , 填補這些 空閒空間 , 就能夠了 。 固然 , 會先檢查 「堆表空閒空間計數器」 , 若是 計數器 值爲 0 , 代表沒有空閒空間 , 不須要 垃圾回收 , 大於 0 表示 有空閒空間 , 須要 垃圾回收 。 若是沒有要 回收的 空閒空間 , 或者 回收了 空閒空間 之後 Append 指針指向的 地址 到 堆表 的結束地址 之間的空間 仍然不夠 存放新的 堆表項 , 怎麼辦呢 ? 對於 固定大小的 堆表 , 則 拋出異常 「堆表超出最大範圍」 , 就像 StackOverflow 或者 OutOfMemory 。 對於 能夠無限增加的 堆表 , 則 新申請一塊 堆表 空間 , 繼續工做 。 新的 堆表空間 和 原來的 堆表 空間之間 經過 鏈表 的 方式 相連 。

 

一個 堆表空間 包括 3 個部分 組成 :

1 一塊連續的內存空間

2 Append 指針

3 堆表空閒空間計數器

 

要 申請新的 堆表空間 , 須要提早進行 , 不要等到 空間不夠用 的時候再進行 。 這是由於 新的 堆表空間 的申請 一樣也是 經過 堆 的方式進行 , 一樣須要在 堆表 裏 記錄 堆表項 (Free Space 項 和 索引項)。 當某一次 申請 或 回收 須要記錄 堆表項(Free Space 項 和 索引項) 而 空間不夠時 再去 申請 新的堆表空間 , 則 本次應用程序的申請或者回收 所產生 的 堆表項 (Free Space 項 和 索引項) 和 申請 新的 堆表空間 所產生 的 堆表項 (Free Space 項 和 索引項) 要放在一塊兒計算 和 存儲 , 這樣狀況很複雜 。

 

因此 , 應用程序的申請和回收 內存塊 , 和 申請 新的 堆表空間 , 應該是 2 次 獨立操做 。 因此須要 提早進行 「未雨綢繆」 。 提早到什麼程度呢 ? 在 原來的 堆表空間 的剩餘空間 還 足夠 存儲 一次 申請內存塊 產生的 可能的 最大數量的 堆表項 (Free Space 項 和 索引項) 的時候 。

 

申請一次 內存塊 可能產生多少 堆表項 (Free Space 項 和 索引項) ? Free Space 項容易理解 , 上文也分析過 。 那麼會產生多少 索引項 ?

上文中提到能夠 建立 2 個索引 : 1 Free Space 起始地址 做爲檢索條件 的索引 , 2 Free Space Size(空間大小) 做爲檢索條件 的 索引 。

索引 1 能夠用作 回收時 查詢 和 回收的內存塊 相鄰的 Free Space , 若是 2 者是 相接 的 , 則會進行 合併 。

索引 2 能夠用作 分配時 查找 Size(空間大小) 最接近 申請內存塊大小 的 Free Space 。

但實際上 , 索引 的 建立 也是 比較消耗時間的 , 分配 能夠採用前文最先提出的 先在 當前 Free Space 中分配 , 若當前 Free Space 的空間大小不足以分配 , 則 查找下一個 Free Space 分配 , 以此遞推 。 在 內存空間 充裕的條件下 , 這種方式比查找 索引 快 , 同時避免了 建立索引 消耗的時間 。

 

咱們接下來就來 分析 索引的 建立 和 查詢 :  

根據上述 , 咱們只會創建和使用 索引 1  , 用於 回收 時 合併 相接 的 Free Space 。

索引 1 在 分配時 建立(更新) , 在 回收時 查詢 並 更新 。

索引 1 的 索引項 是 這樣 : 最高位字節 用來保存 索引項的值 , 只會用到 低位 的 2 位 ,表示 4 種狀況 : 00 , 01 , 10 , 11 。 後面再跟 4 個字節 或 8 個字節 表示 指向的 子索引項 或者 Free Space 項 的 地址 。 若是是 32 位 或 「Any CPU」 應用程序 , 則是 4 個字節 , 若是是 64 位 應用程序 , 則是 8 個字節 。

在 分配 時 , 用於 分配的 Free Space 的 大小(Size) 和 起始地址 會發生變化 。 對於 索引 1 , 只需根據 起始地址 來 更新索引 便可 。

Free Space 的 起始地址 字段 表示 空閒空間 的 起始地址 。 同上 , 若是是 32 位 或 「Any CPU」 應用程序 , 則是 4 個字節 , 若是是 64 位 應用程序 , 則是 8 個字節 。 根據 《我發起了一個 .Net 開源數據庫項目 SqlNet》  http://www.javashuo.com/article/p-gpuysmcv-q.html    文中對於 索引 的 論述 , 對於 32 位的數據 , 會創建 32 / 2 = 16 個索引項   -_-   , 對於 64 位的數據 , 會創建 64 / 2 = 32 個索引項   -_-      。

因此 , 對於 32 位 或 「Any CPU」 應用程序 , 分配時 Free Space 起始地址 發生變化 須要修改 索引 最多須要 約 16 個索引項 , 或者說 時間花費是 O(16) 。 由於 檢索 1 個 索引項 須要 判斷 4 種狀況 : 00 , 01 , 10 , 11  。 因此咱們能夠假設 1 次操做的時間是 4ns (4 納秒) , 那麼 O(16) 的時間就是 16 * 4 = 64 ns (64 納秒) 。 而 回收 須要查找索引找到 和 回收的內存塊 相鄰的 Free Space , 同時 回收後 可能更新相鄰 Free Space 的 起始地址(合併) , 或者 產生一個 新的 Free Space , 對於前者 , 須要修改索引 , 對於後者 , 須要建立索引 , 但無論是哪一種 , 最多須要檢索(修改)的 索引項 約 16 個 , 能夠認爲 時間花費 是 O(16) , 而 回收 時查找索引尋找相鄰 Free Space 的 時間花費 也能夠認爲是 O(16) , 因此 加起來就是 回收 的 時間花費 是 O(16) + O(16) = O(32) , 同上 , 假設 1 次操縱的時間是 4ns , 則 回收 的時間花費是 32 * 4 = 128 ns (128 納秒) 。 固然 分配 和 回收 具體花費的時間還會 包括 修改 Free Space 起始地址 , Next 指針 , 合併時 刪除 多餘的 Free Space 項 等 , 這些先忽略不計 , 在下面估算的時候會酌情估算進去 。    

一次 分配 的時間是 64ns , 再加上 分配 時 可能發生的一些遍歷 (在 當前 Free Space 的大小不夠時 , 訪問下一個 Free Space 嘗試分配 , 以此遞推) , 就按 80ns 算 ,  1 秒鐘 大概能夠進行  1200萬次 分配 。 如何 ? 還行吧 , 呵呵 。 不過比起我想象中的 new , 仍是 慢了一點 , 我想象中的 new 應該是 1ns   new 一個嘛 !  P:  new 就是 分配 。

一次 回收 的時間是 128ns , 就按 150ns 算 , 1 秒鐘 大概能夠進行 600萬次 回收 。 能不能再快一點 ?   ^^

 

對於 64 位 應用程序 ,  時間花費 是 32 位 的 2 倍 , 因此 1 秒鐘 能夠分配 600萬次 , 回收 300萬次 。 如何 ? 哎 ? 爲何 64 位 反而慢了 ?

 

上面的 分配 和 回收 的 執行速度 是 針對 1 個 CPU 核 分析的 , 但對於多核 , 分配 和 回收 的 執行速度 也是 如此 。 由於 堆 是進程內全部 線程 共享的 , 堆表 也是共享的 , 在進行 分配 和 回收 時要修改 堆表 , 此時須要對 堆表 進行 同步/互斥 (Lock) , 因此 , 對於多核 , 分配 和 回收 的 執行速度 也是 如此 。

從這裏能夠看出 , 堆 的這一特性會成爲 瓶頸 。 在 高頻 高密度 計算的 場合 。 好比 高併發 實時 響應式 系統 。 說的直接一點 , 就是跟如今的 互聯網 大規模 計算 有關 。  

這一類型的 瓶頸 也表如今 其它方面 。 好比 套接字(Socket) , Socket 對於每一個網卡只會有一個 線程 負責從 網卡 讀寫數據 。 這是個人 推測 。 一個 端口(Port) 的 Socket 由一組線程組成 : 1 負責從網卡讀寫數據的線程(1 個網卡 對應 1 個線程) , 2 處理和分發數據給應用程序的線程們(有若干個線程 , 線程數 和 CPU 的 核數對應 , 能夠包括 虛擬線程(超線程) 數) 。 在 線程 1 和 線程 2 們 協做 的時候 , 會有一個共享數據區 , 線程 1 會把從 網卡 讀取到的 數據 放到 共享數據區 , 線程 2 們 會從 共享數據區 取出數據處理分發 。 顯然 , 線程 1 和 線程 2 們 的協做須要 同步/互斥(Lock) , 

咱們能夠看一下這篇文章《面向對象編程的弊端是什麼?》 https://www.zhihu.com/question/20275578/answer/136886316?utm_source=com.tencent.tim&utm_medium=social&utm_oi=697587017629851648

文中有一幅圖  :    

如圖 紅線 所示 , Mutex(同步 / 互斥 Lock) 的時間是 17ns (17 納秒) 。 這個時間是一個 不太能忽視 的 時間 。

因此 , 這會成爲 利用 並行計算 大幅提高計算能力的 瓶頸 。 而 利用 並行計算 大幅提高計算能力 正是 當下和將來 的 主題 。

另外就是 , 一個網卡只有一個 IO 線程 , 這也可能成爲 瓶頸 。 當網絡技術發展到 5G 或 6G 的時候 , 會不會有 NPU(Net Process Unit)出現 ? 就像 GPU 同樣 。  ^^      

 

實際上 , 對於 堆表 的無限增加 , 有一個 「終極」 的解決辦法 , 或者說 更好的辦法 。 就是 GC (垃圾回收器) 。

在 現代 , 或者說 「當代」  的 語言 , 如 C# , Java 裏都有 GC 。 GC 能夠將 Free Space 的 數量 控制在 有限 和 不多 的 範圍 。 這樣就不存在 堆表 的 無限增加 了。  

而後 。

固然 , GC 要登記 全部變量 , 並按期遍歷 , 移動數據 , 這些也要花費時間的 。

 

堆表 的 無限增加 , 這是一個問題 。 堆表 增加 , 表示 Free Space 增多 , 碎片 也增多 , 這樣 在 分配 時可能會遍歷 比較多的 Free Space 。 

對於 64 位 應用程序 , 64 位 理論上的 尋址空間 能夠達到 16eb , 若是 應用程序 對於 存儲空間 的使用是沒有限制的 , 那麼 , 一段時間以後 , 堆表 , 或者說 Free Space (包括碎片) 的 數量 可能會達到 很大的 數量 。

假想一下 , 若是 Free Space 不少 , 碎片也不少 , 那麼可能要遍歷 不少次 才能找到 大小足夠的 Free Space 進行分配 。 這個時候 , 咱們能夠考慮加入這樣的算法 , 最多遍歷 10 個 Free Space , 遍歷了 10 個 Free Space 還找不到大小足夠的 Free Space , 則 向操做系統 申請 1 個 新的 div , 並將 div 做爲 新的 Free Space 插入到當前位置 , 並從這個 div(新的 Free Space) 中分配 。 分配之後 , 下一次分配固然也會從這個 div 開始 , 若是這個 div 的 剩餘空間 不夠 , 則 訪問下一個 Free Space 。 若是訪問了 10 個 Free Space 也找不到足夠大小的 Free Space , 則 重複上述流程 , 向操做系統 申請 1 個 新的 div , 並將 div 做爲 新的 Free Space 插入到當前位置 , 並從這個 div(新的 Free Space) 中分配 。 以此遞推 。

這種方式 , 可能會浪費一些空間 , 或者說 ,  會向 操做系統 申請多一些的 空間(div) , 可是在 時間 上提升了效率 。 這也算是 「空間換時間」 吧 。 在 如今來說 , 硬件容易擴充 , 提高計算速度 是一個主要目標 。

 

根據以上 , 咱們再來整理一下 具體的 作法 。

 

咱們 以 64位 應用程序 的 標準 來實現 :

當進程啓動時 , 會分配一塊 固定大小 的 連續空間 ,做爲 堆 的 基礎元數據區 , 基礎元數據區 包括 5 部分 :

 

1  Append 指針 , 指向 堆表 可插入 堆表項 的 地址 (當前 最後一個 堆表項 以後) , 插入 堆表項 後 , Append 指針 會 指向 堆表項 結束地址 的 下一個地址 。 Append 指針 的 初始值 應指向 第 5 個 堆表項 的 起始位置 。 由於會在 堆表 中 預先創建 4 個 1 級 索引項 , 見 下面 第 4 部分 。   

2  堆表 的 Free Space 項 鏈表 頭指針 , 指向 Free Space 項 鏈表 的 頭 。 (Free Space 項 之間經過 鏈表 的方式鏈接起來)

3  當前 Free Space 項 指針 , 指向上一次用於 分配 的 Free Space 項 。 下一次 分配 會先嚐試在 上一次 分配 的 Free Space 中進行 , 若 Free Space 的 大小不夠 , 會 訪問 下一個 Free Space 嘗試分配 。 分配 成功後 , 當前 Free Space 項 指針 會指向 分配 成功的 Free Space 項 。 固然這裏面還有些具體的邏輯 , 好比 訪問 超過 10 個 Free Space 項 仍然找不到 大小足夠 的 Free Space , 則 會向操做系統 申請 新的 div , 做爲 Free Space 加入進來 , 而後在這個新的 div 中 分配 。

4  堆表  的 初始空間  。 堆表 的 初始空間 能夠是   1 MB    。 進程啓動 時 , 會初始化 基礎元數據區 , 此時應在 堆表 的 第 1 ~ 4 個 堆表項 位置 預先創建 1 級 索引項 (00 , 01 , 01 , 11) 。 所謂  初始空間  是指這部分是 固定不變 的 , 以後 堆表空間 不夠用時 , 會在 堆 中申請新的 堆表 空間 。 這些新申請的 堆表空間 空出來的時候會 歸還 堆 , 但 初始空間 是 不變的 , 不變是指 一直存在 , 大小不變 。 且 初始空間 不屬於 堆 。

5  Next 指針 , 指向 下一個 堆表 空間 。 隨着 堆 的規模的增加 , 堆表 大小不夠時 , 會從 堆 裏 申請 新的 堆表 空間 , 新的 堆表空間 會和 初始空間 用 鏈表 的方式鏈接起來 , 能夠 申請 多個 堆表空間 ,  如 :   初始空間 -> 第 1 個新申請空間 -> 第 2 個新申請空間 -> 第 3 個新申請空間 -> …… 第 n 個新申請空間 ->  ……

    當 堆 的規模縮小時 , 會釋放 空閒 的 堆表空間 (歸還 堆) 。

    初始空間 不屬於 堆 , 固然永遠不會釋放 。

 

接下來 , 咱們這樣來定義堆表項 : 

堆表項 分爲 2 種 :

1  索引項

2  Free Space 項

具體規則是 : 

1)  索引項 和 Free Space 項 都佔用 34 個字節 。 第 1 個字節 是 標識字節 , 爲 1 表示 索引項 , 爲 2 表示 Free Space 項 , 爲 0 表示 已刪除 。   

2)  對於 索引項 , 第 2 個 字節表示 索引值 , 就是   00 , 01 , 10 , 11   這 4 種值中的一種 , 實際上這 4 種值只用到了 2 位 , 不過咱們仍是用一個字節來存儲 。 若是是 十進制 表示這 4 個值 , 就是   0 , 1 , 2 , 3   。 咱們設計的是 4 階索引 , 第 3 ~ 10 個字節存儲 第 1 個 子索引項 或 Free Space 項 的 地址 (64 位地址 用 8 個字節存儲), 第 11 ~ 18 個字節存儲 第 2 個 子索引項 的 地址 , 第 19 ~ 26 個字節存儲 第 3 個 子索引項 的 地址 , 第 27 ~ 34 個 字節存儲 第 4 個 子索引項 的 地址 。 若 8 個字節表示的 64 位地址 (ulong  無符號長整型  uInt64) 爲  0 , 表示 子項 不存在 。 有關 索引 和 4 階索引 , 我在 《我發起了一個 .Net 開源數據庫項目 SqlNet》  http://www.javashuo.com/article/p-gpuysmcv-q.html    一文中有論述 。

      因此 , 能夠看出 , 索引項 長度 是  1 + 1 + 8 + 8 + 8 + 8 = 34 個字節 。

3)  對於 Free Space 項 , 第 2 ~ 9 個字節 表示 起始地址 , 第 10 ~ 17 個字節 表示 結束地址 。 第 18 ~ 25 個字節 表示 所在的 div 的起始地址 。 第 26 ~ 33 個字節 表示 Next 指針 指向 下一項 Free Space 項 (Free Space 項 之間會經過 Next 指針來用 鏈表 的方式鏈接起來) 。 Free Space 項 的 長度 是  1 + 8 + 8 + 8 + 8 = 33 個字節 。

      爲了便於管理 , Free Space 項的長度也定義爲 34 個字節 , 和 索引項 同樣 。 多出來的 1 個字節 不會用到 。

      將 索引項 和 Free Space 項都定義爲 34 位 是 便於管理 , 或者說 便於算法處理 。 堆表 進行垃圾回收的時候 , 只須要每隔 34 個字節檢查一次 標識字節 , 就能夠知道 堆表項 是否已刪除 , 若 已刪除 則將後面的 堆表項 移動上來 , 填補 已刪除 的 空閒空間 。 這就是 堆表 的 垃圾回收 。

 

div , 接下來講明 div 的定義規則 。 div 是 進程向 操做系統 申請 的一塊 大的 內存區域 , 用於做爲 堆空間 。  

第 1 次 分配 內存塊 時 會申請 第 1 塊 div 。 若是歷來沒有 申請 過 內存塊 , 則不會申請 div 。

 

div 分爲 3 個部分 :

1  結束地址 , div 的 結束地址 , 用  8 個字節表示  (ulong 無符號長整型 uInt64)

2  分配計數器  useCount , 用於記錄 分配 的內存塊 數量 , 若 計數器 的值爲 0 , 表示 div 徹底空閒 , 即沒有 分配 任何空間 , 能夠 歸還 操做系統 。 固然 剛申請到 div 的時候 , 計數器 的值也是 0 , 不過那時會接着用於 分配 。 計數器  也用  8 個字節表示 (ulong 無符號長整型 uInt64)

3  剩餘的空間 用於 分配 。

 

接下來講明 運行邏輯 :

咱們先 估算一下 , 1 MB 的 堆表 空間 夠存放多少個 Free Space 項 (包含 索引項)  ?

Free Space 項 的 地址是 64 位地址 , 要爲 64 位地址 創建 索引 , 須要 64 / 2 = 32 個 索引項 。 每一個 索引項 佔據的空間是 34 個字節 , 再加上 Free Space 項 佔據 的 34 個 字節 , 1 個 Free Space 須要的 存儲空間 是   (32 + 1) * 34 =  1122    個字節 。 實際中會比   1122  小 , 由於 索引 的 父節點 存在共用的現象 。 咱們能夠按  1024  來算 , 存儲一個 Free Space 須要  1024  個字節(包含 索引項) , 那麼  1 MB  能夠 存儲   1024   個  Free Space(包含 索引項) 。

因此 , 1 MB 的 堆表 能夠記錄   1024   個 Free Space , 若是 應用程序 申請 和 歸還 內存塊 產生的 Free Space 不超過 1024 個的話 , 1 MB 的 堆表就夠了 。 若是超過 , 則須要 申請 新的 堆表 空間 。 新的 堆表 空間 在 堆 中申請 。 能夠仍然申請   1 MB 。 若是 新申請 的 1 MB 堆表空間 用完了 , 能夠繼續申請 1 MB , 以此遞推 。 固然 , 實際中 不會等到 堆表空間 不夠用時纔去申請新的 堆表空間 , 上文分析過 , 若是這樣的話 , 會陷入 「本身描述本身」 的 循環中 , 因此 , 應該在 快用完(至少還足夠保存一次申請產生的 最大的 Free Space 變化 ( 包含 索引項 ) ) 的 堆表 空間 時 就申請 新的 堆表空間 。

 

當 應用程序 第 1 次 申請 內存塊 時 , 堆管理程序 會 檢查 基礎元數據區 的  第 1 個 div 的 起始地址 , 若 爲 0 (div 不存在) , 就向 操縱系統 申請 div , 申請到後將 div 的 起始地址 記錄到 基礎元數據區 的  「第 1 個 div 的 起始地址」 。

而後 , 將 div 的 第 3 部分 (用於 分配 的空間) 做爲 1 個  Free Space 記錄入 堆表 (這是 第 1 個 Free Space) 。 固然 , 記錄的操做 包括 了 創建 索引 。 注意 , 1 級索引項 (00 , 01 , 10 , 11) 固定存儲在 堆表 的 第 1 ~ 4 個 堆表項 位置 。 應用程序啓動 , 初始化 基礎元數據區 時應預先建好這 4 個 索引項 。 

接下來 , 就開始在 堆表 中訪問 Free Space 進行分配 , 固然 如今只有 1 個 Free Space , 就是上面剛添加進去的 Free Space 。 分配的話 , 就從 Free Space 的 起始地址 開始分配 。 好比 , 要 申請 1 KB 的 內存塊 , 那麼就把  Free Space 起始地址 ~ Free Space 起始地址 + 1 K - 1    這塊內存 分配 給 應用程序 。 若是 申請的 內存塊大小 比 這個 第 1 個 Free Space 都大 , 那麼應該拋出異常 「只容許申請大小在 xx 範圍內的內存塊」 。

分配 的 具體工做 : 修改當前 Free Space 的 起始地址 , 修改成 Free Space 起始地址 + 1 K , 同時 修改索引 , 根據 Free Space 原來的 起始地址 遍歷 索引項 , 遍歷到 和 新的 起始地址 不一樣 的 索引項 就修改 索引項 。 這麼說好像不知道在說什麼 。好吧 , 咱們舉個具體的例子 :  

咱們的設計是 64 位地址 , 舉例的話 就 簡單一點 , 咱們 以 8 位地址 爲例 , 假設 Free Sapce 的 起始地址 是 0  (0000 0000), 申請 4 個字節大小的內存塊 。

申請前 Free Space 的 索引是這樣的 :  00 -> 00 -> 00 -> 00 , 申請後 Free Sapce 的 起始地址 會變成 4 (0000 0100) , 相應的 , 索引會變成 :  00 -> 00 -> 01 -> 00 , 能夠看到 , 從 第 3 個索引項 開始 , 新的索引 和 舊的索引 變得不一樣 , 因此 咱們 從 第 3 個 索引項 開始修改 爲 新的索引項 就能夠了 。

整個修改索引的過程 會 遍歷 所有的索引項 (包含了 修改) , 64 位地址 是 32 個 索引項 , 因此 分配 的 時間複雜度 約大於 O(32)  (還要考慮其它的操做 , 因此是 約大於) , 咱們上文中就是這樣估算的 。

其它還有什麼操做呢 , 好像沒有了 。  ^^

分配就 2 步操做 :  1  修改 Free Space 起始地址  ,  2  修改索引    。

 

接下來是 歸還 , 歸還 分爲 4 種狀況 :

1  歸還 的 內存塊 的 先後 不和 已有的 Free Space 相接 , 這樣 歸還 會產生 一個 新的 Free Space 。

2  歸還 的 內存塊 和 前面 或者 後面 已有的 Free Space 相接 , 這樣 須要 和 相接的 Free Space 合併 。

3  歸還 的 內存塊 和 前面 和 後面 已有的 Free Space 相接 , 這樣 須要 和 先後 2 個 Free Space 合併 。

4  歸還 的 內存塊 沒有 相鄰 的 Free Space , 這種狀況比較特殊 , 這種狀況就是 整個 div 的 內存 徹底被 分配 出去的 狀況 。

具體 流程 是這樣 :

應用程序 將 內存塊 的 起始地址 提供給 堆 來 歸還 這塊內存塊 。 堆 根據 內存塊 的 起始地址 查找索引 , 查找 和 內存塊 前相鄰 的 Free Space 。 前相鄰 , 是指 相鄰 且 在 前面 。 什麼是 前面 ? Free Space 的 起始地址 小於 內存塊 的 起始地址 叫 前面 , 大於 叫 後面 。

根據 索引 查找到 前相鄰 的 Free Space , 還不必定是 真正 的 前相鄰 的 Free Space , 還要加一個 判斷條件 :  Free Space 所在的 div 和 內存塊 所在的 div 是 同一個 div , 這樣纔是 前相鄰 的 Free Space 。

 

咱們這樣來 定義 前相鄰 後相鄰 :

前相鄰 :  起始地址 小於 內存塊 的 起始地址 , 且 和 內存塊 屬於同一個 div , 則爲 前相鄰 。

後相鄰 :  起始地址 大於 內存塊 的 起始地址 , 且 和 內存塊 屬於同一個 div , 則爲 前相鄰 。

 

若是 查找不到 前相鄰 , 那麼就根據 基礎元數據區 裏的 Free Space 鏈表 頭指針 找到 頭指針 指向 的 Free Space 項 , 這個 Free Space 項 就是 內存塊 的 後相鄰 。

若是 Free Space 鏈表 頭指針 爲 空 (0) , 也表示 沒有 相鄰 (既沒有 前相鄰 , 也沒有 後相鄰) 。

什麼狀況下 Free Space 鏈表 頭指針 爲 空 (0)  呢 ? 在 應用程序 初始化 後 , 尚未 分配 的時候 。 以及 分配 之後 , 整個 div 都被分配出去 。 若是有多個 div , 全部 div 都被徹底的分配出去 , 頭指針 也爲 空 (0)  。

頭指針 不空 , 能夠找到 起始地址 大於 或 小於 內存塊 起始地址 的 Free Space , 但 Free Space 和 內存塊 不在同一個 div 的話 , 也不是 相鄰 。

怎麼判斷 Free Space 和 內存塊 在不在 同一個 div ?  Free Space 項 有一個字段 是 所在 div 的 起始地址 , div 的 第 1 個 部分 是 div 的 結束地址(見上文對 div 的定義) , 根據 div 的 起始地址 能夠找到 div 的 結束地址 , 根據 div 的 起始地址 和 結束地址 能夠判斷 內存塊 在不在 div 裏 。

 

找到 前相鄰 後 , 判斷 前相鄰 的 結束地址 + 1 和 內存塊 的 起始地址 是否相等 , 若相等 , 則 二者應合併 。 但這裏還要進一步的判斷 , 是 狀況 2 仍是 狀況 3 , 因此 還須要 根據 前相鄰 的 Next 指針 找到 下一個 Free Space 項 , 這就是 後相鄰 。 判斷 後相鄰 的 起始地址 和 內存塊 的 結束地址 + 1 是否相等 , 若相等 , 表示是 狀況 3 , 若不等 , 表示是 狀況 2 。

若是 沒有 相鄰的 Free Space , 就是 狀況 4 。 若是有 相鄰的 Free Space , 但既不是 狀況 2 , 也不是 狀況 3 , 就是 狀況 1 。

 

對於 狀況 1  , 須要 新建一個 Free Space 項 , 插入到 Free Space 項 鏈表 裏 , 插入位置是 內存塊 的 前相鄰 以後 , 或者說 , 後相鄰 以前 。 固然 , 新建 Free Space 項 須要創建 相應 的 索引 。 索引 有 32 個 索引項 , 因此 新建 Free Space 的時間複雜度 約大於 O(32) 。再加上 查找 前相鄰 的時間複雜度 O(32) , 因此 狀況 1 的 時間複雜度 約大於 O(32) + O(32) = O(64) , 約大於 O(64) 。 上文就是這樣估算的 。

對於 狀況 2 ,  若是和 前相鄰 相接 , 就 修改 前相鄰 的 結束地址 和 索引 就能夠 , 若是和 後相鄰 相接 , 修改 後相鄰 的 起始地址 和 索引 就能夠 , 這個和 分配 的 操做方法 同樣 , 參考上文 分配 的部分 就能夠 。

對於 狀況 3 , 能夠 修改 前相鄰 的 結束地址 和 索引 , 同時 刪除 後相鄰 , 相應的 , 後相鄰 的 索引 也要刪除 。 刪除索引 的 步驟是 :  根據 後相鄰 的 起始地址 遍歷 索引項 , 對於只有 1 個子索引項 的 索引項 刪除 便可 。 只有一個 子索引項 表示 從 當前索引項 開始的 索引路徑 僅僅指向 要刪除的這個 後相鄰 。

對於 狀況 4 , 直接按照 內存塊 的 起始地址 結束地址 新建一個 Free Space 項 , 添加到 Free Space 堆表 , 固然會創建相應的 索引 。 同時 , 還要將 Free Space 項 插入 Free Space 項 鏈表 裏 。 插入位置 在 ——    根據 索引 查找出 起始地址 小於 本身 的 Free Space 項 , 插入到這一項以後就行 。 注 :  由於不在同一個 div , 因此 不能叫 前相鄰 或者 後相鄰 。 若是 查找不到 起始地址 小於本身的 , 就插入到 頭 , 即 基礎元數據區 裏的 Free Space 鏈表 頭指針 指向 本身 , 本身 的 Next 指針 指向 原來 頭指針 指向 的 那一項 。 若是 頭指針 原來是 空 (0) , 那就 讓 頭指針 指向 本身 就能夠了 。

 

Free Space 項 鏈表 不是一個 獨立 的 東西 , 而是 堆表 裏的 Free Space 項 之間會經過 Next 指針來用 鏈表 的 方式 鏈接起來 。 由於只有 Next 指針 , 因此是 單向鏈表 。 如今看起來 , 單向鏈表 夠用了 。        -_-  '

 

每次 申請 和 歸還 後會檢查是否進行 垃圾回收 , 當知足如下 2 個條件時進行 垃圾回收 :

1  Append 指針 到 堆表 結束地址 的 內存空間 小於 1500 個字節時 ,

2  堆表 的 空閒空間 超過 堆表空間 的 2/3 的時候 

 

每次 垃圾回收 後會檢查是否須要 擴充 堆表, 當知足如下條件時 擴充 堆表 :

Append 指針 到 堆表 結束地址 的 內存空間 小於 1500 個字節時 ,

擴充 堆表 就是 申請新的 堆表空間 和 初始空間 用 鏈表 的方式 鏈接起來 , 固然 , 隨着 堆 的規模的擴大 , 能夠 申請 第 2 個 、 第 3 個 、第 n 個 ……  堆表空間 , 用 鏈表 的方式連起來就是  :    初始空間 -> 第 1 個新申請空間 -> 第 2 個新申請空間 -> 第 3 個新申請空間 -> …… 第 n 個新申請空間 ->  ……

這一點的意義上面已經屢次分析過 , 爲了不陷入 「本身描述本身」 的 陷阱 , 因此須要在 堆表 空間 快用完時 , 擴充 堆表 空間 。 堆表 空間最少要可以存儲一次 分配 (包含 可能 申請 div 的 狀況)  所產生的 Free Space 項 (包含 索引項) 。 通常的 分配 只需 修改 Free Space 項 的 起始地址 和 索引 , 當有 申請 div 的 情形 時 , 會新建 Free Space 項 及 完整的 索引 (32 個 索引項) , 這應該是 分配 時 佔用空間 最大的狀況 , 咱們按這種狀況來計算 。 上面說過 , 1 個 Free Space (包含 索引項) 會佔用 1122 個 字節 , 咱們放寬鬆一點 , 在 堆表 剩餘空間 只有 1500 個字節 時 就 擴充 堆表 。

 

那何時 「壓縮」 或者說 釋放 空閒出來的 堆表 空間 呢 ?

在 垃圾整理 後 , 檢查 最後一個 「不空」 的 堆表空間 , 即 最後一個 存儲了至少 1 個 堆表項 的 堆表空間 , 若是 這個 堆表空間 的 空閒空間 超過 堆表空間 的 2/3 , 那麼將 釋放 這個 堆表空間 以後 全部的 堆表空間 。 釋放 就是 將 堆表空間 歸還 堆 。 上文說了 , 初始空間 之外 的 堆表空間 都是 從 堆 裏申請的 。

初始空間 不屬於 堆 , 顯然 , 永遠不會釋放 。

 

說到這裏 , 顯然 , 「堆表」 是一個 可擴充的 , 由若干個 線性表 經過 鏈表 的 方式 鏈接起來的 數據結構 。

Append 指針 指向的是 最後一個 堆表項 , 這個 堆表項 可能在 初始空間 , 也可能在 新申請 的 第 n 個 堆表空間 。

 

在 分配 時 , 會從當前 Free Space 項 指針 指向的 Free Space 項 開始 嘗試分配 , 若是 當前項 大小不夠 , 會 訪問 下一個 Free Space 項 , 若是 訪問超過 10 個 Free Space 項 還找不到大小足夠的 Free Space , 則 會向操做系統 申請 新的 div , 做爲 Free Space 加入進來 , 而後在這個新的 div (新的 Free Space) 中 分配 。 

這主要是從 執行速度 的角度考慮 。 這也算是 「空間換時間」  。

 

這邏輯真的  亂 ,   煩 。

 

咱們能夠用 文件 的方式來模擬實現這個 堆管理 算法 。

就是用 一個文件 模擬 一塊內存區域 , 來實現這個 堆算法 。

 

咱們會先實現一個 EnLargableList 的數據結構 , EnLargableList 是一個 線性表 經過 鏈表 的方式鏈接起來的 可擴充的 數據結構 , 用來實現 堆表 。

 

堆 的 複雜來自於 堆表 的 動態增加(無限增加) , 若是 堆表 是 固定大小 的 , 那麼 堆 並不太難 。

 

 上面有一個地方的邏輯有漏洞 , 向操做系統申請了一個 div 以後 , 除了 將 div 可分配的空間做爲一個 Free Space 項 加入 Free Space 項 鏈表 外 , 還應該新建一個 「空的」 Free Space 項 加入 。 這個 「空的」 Free Space 項 的 起始地址 和 結束地址 都是 div 的 可分配空間 的 起始地址 。 由於 起始地址 和 結束地址 相等 , 因此是 「空的」 。 由於 大小 是 0 , 老是 小於 申請的內存塊的大小 , 因此 , 在 分配 的時候不會分配這個 Free Space 。

這個 空的 Free Space 有什麼用呢 ?  這是爲了解決 整個 div 都被徹底的分配出去的狀況 , 上文分析過了 , 整個 div 都被徹底的分配出去的話 , Free Space 鏈表 裏就沒有這個 div 的 Free Space , 這樣 當 這個 div 裏的 內存塊 歸還時 , 會找不到 前相鄰 和 後相鄰 , 從而不知道這個 內存塊 是 哪一個 div 的 , 這樣 歸還 的邏輯就有問題 , 就算無論是哪一個 div 而直接將內存塊做爲 Free Space 歸還 , 最終也會致使即便這個 div 已經所有空閒(全部 分配 出去的 內存塊 都 歸還 了) , 可是沒法將 這個 div 歸還 操做系統 。 至關於這個 div 處於 「半遺棄」 的狀態 。由於 它的 Free Space 仍然能夠繼續 分配 和 歸還 , 但 這個 div 已經不在 正式名單 上了 , 沒法在 所有空閒 時 歸還 操做系統 。 固然 , 實際中 這樣的操做是 不容許的 , 由於 Free Space 項最後一個字段就是 指向 本身所在 div 的 起始地址 , 就是說 Free Space 項 應該 知道 本身所在的 div , 若是不知道 , 程序不能運行下去 。

因此 , 每一個 div 必定會有一個 空的 Free Space , 無論 div 的空間如何分配 , 這個 空的 Free Space 會一直存在下去 , 直到 div 歸還操做系統 , 這個 空的 Free Space 纔會被刪除 。

由於咱們沒有專門的表 來記錄 div , 因此這個 空的 Free Space 至關於 div 的表明 , 或者 佔位 。

 

上面的作法仍是有一點問題 。 用一個 「空的」 Free Space 來表示 div 會有一些問題 。 實際上 「空的」 Free Space 不是空的 , 是大小爲 1 個字節 的 空間 。 起始地址 和 結束地址 相等 , Free Space 的大小 = 結束地址 - 起始地址 + 1 = 1 。 因此 , 在 歸還 Free Space 時 , 若是 歸還的 Free Space 和 這個 「空的」 Free Space 相接 , 會和 「空的」 Free Space 合併 , 這又會引出合併後下次分配時 第 1 個字節 不能分配(做爲 「空的」 Free Space) 之類的判斷 , 會把算法邏輯變複雜 。

因此 , 咱們放棄了這種方式 。 正統的作法應該仍是把 div 記錄到 堆表 裏 , 也會爲 div 創建索引 。 也就是說 , 增長一種 堆表項 : div 項 。 標識字節(第 1 個字節) 爲 3 表示 div 項 。 div 項的第 2 ~ 9 個字節存儲 div 的起始地址 。 固然 div 項的長度也是 34 (和 索引項 Free Space 項 相同) , 多餘的字節不會用到 。

這樣 , 在 歸還 內存塊 時 , 若是找不到 前相鄰 , 也找不到 後相鄰 , 說明 div 被徹底分配出去了 , 此時就會根據索引查找 div , 找到 起始地址 小於 內存塊的起始地址 且相鄰 的 div , 這就是 內存塊 所在的 div 。

歸還 內存塊 後 , div 的 分配計數器 會 減 1 , 減 1 後檢查 計數器值 是否爲  0 , 若爲 0 則 div 的空間已徹底空閒 , 因而將 div 歸還操做系統 。

 

但這樣的作法仍是有問題 , 要爲 div 創建索引 , 這有一點額外的麻煩 , 好比 如今的 堆表項 開始的 4 個項位置 存儲的是 4 個 1 級索引項 , 若是要爲 div 創建索引 , 須要專門再爲 div 創建 4 個 1 級索引項 , 這些會增長算法內容 , 會變得複雜或者麻煩 。

因此 , 咱們仍是回到用 一個 「空的」 Free Space 來表示 div , 或者 佔位 的作法 。 在 申請一個新的 div 的時候 , 會建立 2 個 Free Space , 一個是 「空的」 Free Space ,  另外一個是 可用的 Free Space 。 div 的開頭會用 8 + 8 = 16 個字節分別表示 結束地址 和 分配計數器 use Count , 「空的」 Free Space 就是 第 17 個字節 , 起始地址 和 結束地址 都是 第 17 個字節 , 從第 18 個字節開始就是 可用空間 了 , 可用的 Free Space 就是 第 18 個字節 開始到 div 的 結束地址 。

咱們能夠給 Free Space 項 增長一個 字節 來表示 Free Space 的 「Type」 , 在 標識字節 以後 。 第 1 個字節是 標識字節 , 咱們用 第 2 個字節來表示   Free Space Type ,  0  表示 「空的」 Free Space ,  1  表示 普通的 Free Space 。 這樣的話 , Free Space 項 和 索引項 同樣 , 都是 34 個字節了 。

在 分配 和 回收 時 須要判斷 Free Space 時 「空的」 Free Space 仍是 普通的 Free Space 。 上文中定義過 , 標識字節 爲 2 表示 普通的 Free Space 。

在 分配 時 判斷 , 若是 是 「空的」 Free Space , 就不進行分配 , 而是 訪問下一個 Free Space 嘗試分配 。

在 回收 時會尋找 前相鄰 , 若是 前相鄰 是 「空的」 Free Space , 則不進行 判斷是否相接若相接則合併的邏輯 。

 

EnLargableList (用於 堆表)  會設定這樣一些參數:

1  whenRecycleFragment , 這是一個 整數 , 表示 碎片數量 超過多少 應開始 碎片回收 , 能夠設置爲 1萬 , 碎片數量 是 以 對錶項 爲 單位 。 假設 堆表空間 是  1MB , 每一個 堆表項 佔用 34 個字節 , 能夠存約  3 萬個 堆表項 , 約表示 1024 個 Free Space (每一個 Free Space 最多由 33 個 堆表項 表示 , 包含 32 個 索引項 + 1 個 Free Space 項) 。

    若是 設置 whenRecycleFragment 爲  1 萬 , 至關因而 一個 堆表空間 中有 1/3  的 空閒空間 , 此時回收 。 效果怎麼樣 ?  不知道 。

    或者說 至關於 一個 堆表 空間中 記錄了 600 個 Free Space 項 , 還有 300 個 Free Space 的位置能夠記錄 , 此時回收 。 效果怎麼樣 ?  不知道 。

    上文中提到 當 Append 指針 到 堆表空間 的 結束位置 的 空間 小於 1500 時 回收 , 但如今放棄了這種作法 。

    由於 這種作法 好像不太科學 , 在應對 規模很大 的 堆 時候 , 好像不太適用 。 堆 的 規模很大 , 是指能夠無限制的 使用 地址空間 , 內存塊 數量 和 Free Space 數量(包含 碎片)  可能 持續增加 。  大小 1MB 的 堆表 能夠存約  3 萬個 堆表項 , 以 堆表項 爲 單位 遍歷 一遍 須要 遍歷 3 萬個 堆表項 。 3 萬 是一個不小的數量 , 因此咱們想 當 碎片(空閒出來的 項位置)  達到  1 萬 的時候回收 可能會比較好 。

2  whenEnLarge , 這是一個整數 , 表示 append 指針 到 堆表 末尾 的 空間還有多少時 擴充 堆表 容量 , 擴充 堆表 容量 就是 申請新的 堆表空間 , 新申請的 堆表 空間 以 鏈表 的方式鏈接到 當前 堆表空間 。

3  heapTableSpace :   就是每個 堆表 空間的 大小 , 能夠設爲 1MB , 每次申請 新的 堆表空間 就是 申請 heapTableSpace 大小的 一個 內存塊 。

 

EnLargableList  還會 保存這樣一些 字段 :

1  appendPtr , append 指針 , 存儲一個 64位地址 , EnLargableList 寫入數據時從 append指針 指向的數據開始寫 , 每寫入一段數據 , append 指針會移動到這段數據以後的位置 。

2  currentHeapTableSpace , 當前 堆表空間 , 即 append 指針 指向的 位置 所在的 堆表空間 。 這個字段用來 歸還 堆表空間 。 歸還 是指 , 當 末尾一個 堆表空間 , 即 當前 堆表空間 的 空間 所有 空閒出來時候 , 會將 堆表空間 歸還 堆 。 僅僅憑 append 指針 不能知道 append 指針 所在的 堆表空間 , 因此還須要這個字段來記錄 append 指針 所在的 堆表空間 , 即 當前 堆表空間 。

3  recycleFreeItem , 碎片回收 時 指向 空閒的項位置 , 即 「碎片」 , 或者說  「已刪除」的項 。

4  recycleScanItem , 碎片回收 時 會先掃描 「碎片」 , 掃描到一個 「碎片」 以後 , 會將 recycleFreeItem 指向這個 「碎片」 的位置 。 而後會掃描 堆表項 , 每掃描一個 堆表項 , 會檢查 堆表項 的 子項 (子索引項  Free Space項) , 若 子項 的 位置 大於 recycleFreeItem 指向的位置 , 則將 子項 移動到 recycleFreeItem 指向的位置 , 「填補」這個碎片 , 同時修改 當前掃描的 堆表項 中保存的 該 子項 的位置 。 這樣就完成一個 「碎片」 的 回收 (「填補」) 。

    而後就繼續 掃描下一個 「碎片」 , 掃描到 「碎片」 後 , 又接着掃描 上一次 掃描的 堆表項 。 怎麼知道 上一次掃描的 堆表項 ? 就是 recycleScanItem 指向的堆表項 。 不過這樣看起來 , 還要加一個 字段 , 來表示 掃描到了 堆表項 裏的 哪一個子項 , 以下 :

5  recycleScanSubItem , 表示 掃描到的 堆表項 的 子項 。 這個字段只要 8 位整數 就能夠了 。

6  fragmentCount , 表示 「碎片」 數量 , 每次 刪除 堆表項 時 加 1 , 在 碎片回收 「填補」 碎片 的時候 減 1 , 這個字段用於上文中 若是 fragmentCount 的數量達到 whenRecycleFragment 的值 的 時候 , 就開始 碎片回收 。

7  堆表空間 的 useCount , 這個 字段 是 每一個 堆表空間 保存 1 個 , 就是 堆表空間 的 useCount , 就是 堆表空間 使用的計數(以 堆表項 爲單位) 。 每寫入 1 個堆表項 , 就在 堆表空間 的 useCount 加 1 , 每刪除 1 個 堆表項 , useCount 就 減 1 。 useCount 爲 0 表示 堆表空間 

 

每次 分配 和 回收 以後會 檢查 fragmentCount , 當 fragmentCount 超過 whenRecycleFragment 時 會開始回收 。 因爲不但願回收佔用太多時間 , 能夠設定 一個參數好比 recycleItemCount , 好比 300 , 表示 無論有沒有回收完 , 只 掃描 300 個堆表項 。 

但這樣會有一個問題 , 自己要 fragmentCount 超過 whenRecycleFragment 時纔開始回收 , 並且每次又不回收完 , 空閒出來的 碎片空間 得不到重複利用 , append 指針 只能 一直向後移動 , 因此可能致使 永遠回收 不完 , 堆表 持續 增加 。

 

因此        ……

 

咱們這裏有了一個 突破 , 即對於 堆表 的 碎片回收 , 咱們採用了一個 新的算法 , 就是在 堆表項 裏 增長 1 個字段 :   fragmentNext   。

就是把 已刪除的堆表項(碎片) 用鏈表的方式 鏈接 起來 , 這樣每次寫入 堆表項 的時候從 這個 鏈表 的 頭 取出 一個 碎片 , 做爲 新的堆表項 的 寫入位置 。 fragmentNext 表示 下一個 碎片 的 位置 , 或者說 , fragmentNext 是一個指針 , 指向下一個 碎片 。 

實際上 是一個用 鏈表 實現 的 隊列 。

因此 , 須要在 基礎元數據區 裏增長 2 個字段  fragmentListHead  ,   fragmentListTail , 用於保存 碎片鏈表(隊列) 的 頭指針  和   尾指針。

每次 刪除 堆表項 時 , 將 被刪除的 堆表項 的 標識字節 更新爲 0 , 表示 已刪除 , 同時將 堆表項 添加到 碎片隊列 的 尾部 。

若是是 第一次 刪除 , 那麼 碎片隊列 裏 尚未 元素 , 則 將 頭指針 和 尾指針 都指向 堆表項 。

每次寫入 堆表項 的時候 , 會先從 碎片隊列 裏 取得碎片 , 做爲寫入位置 , 若是 碎片隊列 爲空 , 纔會將 append 指針 做爲寫入位置 。

fragmentNext 指針也是一個 64位無符號整數 ( uInt64 ) , 因此也佔用 8 個字節 。 這樣的話 , 索引項 和 Free Space 項 的 大小 都是 34 + 8 = 42 個字節了 。

 

好的 , 如今咱們再來看看在這種算法下 , 如何 回收 碎片 。  (這裏的 「碎片」 是指 堆表 裏的 碎片 , 不是 堆 裏的 碎片)

實際上 , 在這個算法下 , 碎片能夠獲得充分的利用 (每次 寫入 都優先 從 碎片隊列 中取得 碎片 做爲寫入位置 , 碎片隊列 爲空纔會用 append 指針 的方式) , 因此 看起來 堆表 不會無理增加 。 但又一些特殊的狀況 , 好比 應用程序 先申請了 大量的 小塊內存 , 形成了 大量 的 Free Space , 爲了存儲這些 Free Space , 堆表 會變得很大 , 以後 應用程序 又歸還了 全部 或者 大部分 內存塊 , 也是 Free Space 會變得 不多 , 此時 堆表 中就會產生大量 空閒空間(碎片) , 這些 空閒空間 若是 長時間不用又不歸還 堆 , 也是一種浪費 。 

咱們能夠這樣來設計 堆表 的 碎片回收 算法 :

首先 , 只有 碎片數量 大於 某個值 的時候 , 纔會開始回收 。 好比 大於 1000 個碎片(約 1 MB) 。

從 初始空間 開始 , 向後遍歷每個 堆表空間 , 若是 堆表空間 的 useCount 爲 0 , 則能夠考慮 釋放 這個 堆表空間(歸還 堆) 。

注意 , 這裏是 考慮 , 不是必定要歸還 。 還要判斷一個條件 , 就是 堆表 的 可用空間 usableSpace  是否足夠 , 若 足夠 則 釋放(歸還)堆表空間 , 不然不釋放 。 注意 usableSpace 是 整個堆表 的 可用空間 (包括 全部的 堆表空間) 。

堆表 的 初始空間 不屬於 堆 , 屬於 基礎元數據區 , 永遠不會釋放。

因此在 基礎元數據區 中要增長一個字段 usableSpace , 上文的一些算法邏輯也要作一些修改 。

usableSpace 初始值 等於 初始空間 的 大小 。 以後 每申請一個 新的 堆表 空間 , 則 加上 新的 堆表空間 的大小 , 若 歸還 堆表空間 , 則 減去 歸還的 堆表空間 的 大小 。

每次 向 堆表 寫入數據 , usableSpace 加上寫入數據的長度 , 好比 1 個 堆表項 長度是 34 個字節 , 那麼 寫入一個 堆表項 的話 ,   usableSpace += 34;    。

每次 從 堆表 中 刪除數據 , usableSpace 減去刪除數據的長度 , 好比 刪除 1 個 堆表項 , 則    usableSpace -= 34;    。

 

上文中的 append 指針 到 堆表 末尾 的 空間 小於 1500 時 應 擴充 堆表 (申請 新的 堆表空間) 這一段 須要改爲 :

usableSpace 小於 1500 時 , 應 擴充 堆表 (申請 新的 堆表空間) 。 上文中也提到 若是一個 堆表空間 的 useCount 爲 0 , 則 能夠考慮 釋放 這個 堆表空間 , 但要判斷一個條件 , 即 堆表 的 可用空間 usableSpace  是否足夠  。 咱們能夠設定好比 當    usableSpace - 當前考慮釋放的堆表空間的大小  >  50 萬個字節(能夠存儲約 500 個 Free Space 項 (包含 索引項)) 時 , 能夠 釋放 這個 堆表空間 。

咱們上文 設定的 1 個 堆表空間 的 大小是 1MB , 因此 50萬個字節 約等於 0.5 MB , 上面的條件至關因而 釋放了 這個 堆表空間 後 , 堆表 的 可用空間 還有 0.5 MB , 也就是 至關於 還有 半個 堆表空間 。

這些參數 能夠 根據須要 進行設定 , 上面給出的是 參考數值, 也是 舉例 。

概括一下 , 就是 usableSpace 小於 1500 時 應擴充 堆表 ,   usableSpace - 考慮釋放的堆表空間大小 大於 50萬 時 能夠釋放 堆表空間 。 

是否是 更清晰 了  ?

 

碎片回收 應放在一個 另外的 線程 裏 進行 。 (是否是 想起了  GC    -_-   ' )  , 每隔一段時間運行一次(好比 每秒運行一次) , 若是 堆表空間 的數量很大 , 能夠每次只遍歷 幾個 堆表空間 (好比 10 個) , 後面的 下次 繼續遍歷 。 這樣能夠不影響 分配 和 回收 內存塊 的 執行速度 。

(這裏的 「碎片」 是指 堆表 裏的 碎片 , 不是 堆 裏的 碎片)

 

爲了能在 更新索引 時 只上溯到 索引項值 不一樣的 索引項 , 須要再在 索引項 和 Free Space 項 裏再增長 一個 字段 , parentItem , 保存 上一級 索引項 的 地址 , 是一個 ulong 無符號長整型 , 佔 8 個 字節 , 這樣 ,  索引項 和 Free Space 項 的 長度 就是    42 + 8 = 50   了 。

更新索引 時 只上溯到 索引項值 不一樣的 索引項 , 能夠避免 爲了 更新一個 Free Space 項 的 索引項 而 刪除 這個 Free Space 項 的 所有索引項 並 重建所有索引項 。 刪除所有索引項 再重建 可能會比較省事一些 , 但效率上可能會低一點 。

上溯 的 邏輯 是 檢查 上一級 索引項 的 索引值 和 新索引 在 這一層級 的 索引項 的 索引值 是否相等 , 若是 相等, 則 在 這一級 索引項 上 開始 向下創建 新索引 的 索引項 , 若是不等 , 則 檢查 這個 「上一級」 索引項 除了 當前 索引項 之外 還有沒有 其它 子項 , 若是沒有 , 則 刪除 這個 「上一級」 索引項 以後 繼續 上溯 , 若是有 , 則 直接繼續 上溯 。 刪除 「上一級」 索引項 固然 包括了 刪除 當前 索引項 , 實際上 , 上溯 是從 Free Space 項 開始, Free Space 項 是 索引樹 的 最底層 , 也能夠說是 葉子節點 , 也能夠說是 索引 最終指向 的 數據 , 或者說 數據項 。

 

實際上 「上溯」 這個邏輯好像 行不通 , 由於 上溯 到 索引值 和 新索引 在 這一層級 的 索引值 相同 這並不能說明 更上層 的 索引值 和 新索引 的 對應相同 。要知道 更上層 (或者 說 每一層) 的 索引值 是否 和 新索引 的 對應相同 , 須要 一直 上溯 到 頂層(一級索引), 但這和 從 一級索引 自頂而下 好像沒什麼區別 。   啊哈哈

 

爲了簡單起見, 咱們採用 刪除舊索引, 創建新索引 的方式 。 即 更新索引 採用 刪除舊索引 創建新索引 的方式 。

咱們來看一下這樣的作法的 時間花費 : 

對於 申請 內存塊(new), 須要更新用於分配內存塊的 Free Space 的 索引,  按照上述的作法, 更新包括了 刪除舊索引 和 創建新索引, 刪除舊索引 和 創建新索引 的 時間複雜度 均可以認爲是 O(32) , 加起來就是 O(32) + O(32) = O(32 + 32) = O(64)  。 按照咱們在上面的 估算方法,  O(1)  的時間按 4ns (4納秒) 算 , 那麼 申請內存塊(new) 的 時間花費 就是    64 * 4 = 256 ns 。 256 ns 咱們按 300ns 算的話, 1 微秒 就能夠 執行 3.3 次 new 操做, 1 秒就能夠執行 330 萬次 new 操做 。 由於咱們將 256 ns 近似爲 300 ns 計算, 因此能夠認爲 1 秒能夠執行 330 萬次 以上 的 new 操做 。

對於 歸還 內存塊(delete), 分爲 4 種 狀況:

狀況 1 :  歸還的 內存塊 前面 和 後面 都 不和 已有的 Free Space 相接, 因此 不須要 「合併」, 這樣只須要 新建 索引 就行, 時間複雜度是 O(32) , 時間花費 是  32 * 4 = 128 ns , 能夠估算爲 1 微秒 能夠執行 7 次, 那麼 1 秒能夠執行 700 萬次 。

狀況 2 :  歸還的 內存塊 前面 和 已有的 Free Space 相接, 須要 「合併」。  合併 只需 更新 相接 的 Free Space 的 結束地址 就行 。 由於 索引 是 按 Free Space 的 起始地址 創建的, 因此 更新 結束地址 不須要 更新索引, 因此 狀況 2 的 時間複雜度 是  O(1) ,  因爲只是更新結束地址, 能夠認爲 O(1) 的 時間花費 是 1 * 1ns = 1ns ,  1 秒 能夠執行 10 億次 。 我也有點懷疑, 真的這麼簡單嗎 ? 

狀況 3 :  歸還的 內存塊 後面 和 已有的 Free Space 相接, 須要 「合併」。 合併 只需 更新 後面相接的 Free Space 的 起始地址, 因爲 索引 是 按 起始地址 創建的, 因此須要更新索引, 和 申請內存塊 同樣, 更新索引 包含 刪除舊索引 和 創建新索引, 時間複雜度 是 O(64) , 時間花費是 64 * 4 = 256ns ,  1 秒能夠執行 330 萬次 以上  。

狀況 4 :  歸還的 內存塊 前面 和 後面 都和 已有的 Free Space 相接, 須要將 前面 後面 的 Free Space 「合併」 爲一個 。 合併 須要 修改 前面的 Free Space 的 結束地址, 刪除後面的 Free Space 。 修改 結束地址 不須要 更新索引, 因此 只須要 刪除 後面的 Free Space 的索引就行 。 因此 時間複雜度 是 O(32) , 和 狀況 1 同樣, 時間花費 是 32 * 4 = 128 ns ,  1 秒能夠執行 700 萬次 。

 

哎 ?  我剛又想到一個 好主意, 申請內存塊 的 時候爲何不從 Free Space 的 結束地址 分配呢?  若是 從 Free Space 的 結束地址 分配的話, 就不用 更新索引, 只要修改 Free Space 的 結束地址 就能夠了。 這樣 就和 歸還 的 狀況 2 同樣, 時間複雜度 是 O(1) ,  時間花費 是 1 * 1ns = 1ns ,  1 秒 能夠執行 10 億次 。  (1 秒 能夠 new 10 億次)

上面的 討論 是 從 起始地址 開始 分配 內存塊 的, 因此 每次 new 的時候 會 更新 起始地址, 也就會 更新索引 。

若是 換成 從 結束地址 一端 來 分配內存塊 的話, 就不須要 更新 起始地址, 也就 不須要 更新索引, 能夠大大提升效率 。

固然 這是在 Free Space 的大小足夠分配的狀況下, 若是 Free Space 的大小不夠, 會向後尋找 Free Space, 若尋找了 10 個 Free Space 還未找到 大小足夠 的 Free Space, 則會向 操做系統 申請 div 。 在這些狀況下, 還須要考慮這些 時間花費 。

 

由於 不須要 上溯, 因此 索引項 和 Free Space 項 不須要 保存 上一級索引項 的 位置(地址), 也就是不須要 parentItem 這個字段, 這樣的話, 索引項 和 Free Space 項 的 長度 就 從 50 個字節 變回 50 - 8 = 42 個字節了。

實際上, 咱們在 索引項 裏 設計了一個字段 用來保存 索引值, 但後來發現, 由上一級索引保存的 4 個 子索引項 的 指針字段 能夠 直接指向 子索引項, 子索引項 好像不須要 保存 索引值 。

 

我 這個 設計 是 不會 回收 堆裏 的 碎片 的 。 這 跟 C# Java 之類 有 GC 的 不一樣 。 我想 C++ 也不會 回收 堆 裏的 碎片 。 上文提到的 「碎片回收」 是 回收 堆表 裏的 碎片 , 不是 回收 堆 的 碎片 。 因此 不存在 「全盤整理」 。 每次 歸還內存塊 的時候 會檢查 div 的 useCount ,  每次 分配 內存塊 的時候, 這個 內存塊 所在 的 div  會 useCount ++ , 每次 歸還內存塊 , 這個 內存塊 所在的 div 會 useCount --   。 若是 useCount == 0 , 則將 div 歸還 操做系統 。 但 這種狀況 機率 可能 不大 , 由於 一旦 div 投入使用後, 分配出去 的 內存塊 必須 所有 釋放, div 纔會空(useCount == 0) , 才能 歸還 操做系統 。 但 在 實際使用中, div 投入使用後, 有 申請 有歸還, 所有清空 的 機率 可能不大, 很長時間後, 可能 還有一些 「零碎」 的 內存塊 佔據着, 即便 是 少許的 內存塊, 也 致使 div 不能歸還 。 這就是 C++ 這一類 靜態 作法 的 侷限 。 可能致使 大塊 內存 區域(div) 被 進程佔據, 沒法 迴歸到 操做系統 層面, 形成 資源的 浪費 。

因此, 要解決 這種 靜態作法 的 侷限, 就須要 引入 GC 這樣的 動態特性 。 我想, 當初 GC 的 出現 (以 Java 爲 表明) , 不只僅 是 爲了解決 「內存泄漏」 的問題 , 其實 也 隱藏了 上述 靜態作法 的 種種 侷限 的 緣由 吧 !

固然, GC 的作法 會增長 工做量, 會 花費 時間, 可是, GC 確實 能夠有效 的 控制 堆碎片 數量 和 堆表大小 。 就是說, GC 能夠 使 堆碎片 控制在一個 有限的 範圍內, 使 堆表大小 控制在 一個 有限的 範圍內 , 這 自己 就 簡化了問題, 減小了 管理 開銷 和 複雜度 。 從 這個 角度來說, GC 又是 減少了 時間花費, 提高了 效率 的 。

因此, 從 技術 進步 或者 進化 的 角度 來看, GC 是一次 進化, 使得 能夠用 更現代 更高級 的 方法 來 管理 存儲資源 。

相較之下, C++ 的 靜態作法, 是 早期 和 樸素 的 。

在 現代 存儲資源 能夠 大幅 甚至 無限擴展 的 情形下, 或許 確實 須要 GC 這樣 「動態」 的 方式 來 管理 存儲資源 。 靜態的 方式 面對 大幅 存儲資源 可能 會 有 侷限 。

固然, 在本文中設計的這種 「靜態」作法, 實際上 也是 利用了 現代 存儲資源 大幅提高 的 特色, 比較多 的 應用了 「空間換時間」 。

但確實 存在一個 問題, 就是 靜態的 作法 沒法 控制 碎片 的 增加, 包括 堆碎片, 甚至 堆表碎片, 或者說 不能有效控制 堆表大小 的 增加 。 本文的作法 能夠回收 堆表 碎片, 可是 效果如何, 不知道 。 只要 堆表空間 裏還有一個 堆表項, 就不能 釋放 堆表空間(歸還堆), 這是一個 機率問題 。

因此, 要 準確 有效 的 管理 存儲資源, 仍是 須要 GC 這樣的 「動態」 作法 。

所謂 「動態」, 套用一個術語, GC 創建了一個 「抽象層」 。 

由於 有 這個 「抽象層」, GC 能夠 移動 進程中 的 變量位置, 而 對於 程序來說, 沒有感受到 變化 。

也正由於這樣, GC 能夠 有效的 控制 堆碎片 的 數量 和 堆表大小 在一個 有限 的 範圍 。

在 C++ 裏, 因爲 C++ 比較 直接 的 面向 「底層」(操做系統), 因此, C++ 不能提供 GC 這樣的 「抽象層」, 對於 堆管理, 也就只能使用 「靜態」的作法, 如上所述 。

 

但 到 目前爲止, 上面說的 設計 解決了 基本 的 分配 和 回收 (包括 索引機制, 索引機制 確保了 檢索操做的 時間花費 在一個 已知的範圍內), 但還存在一個重要的問題, 就是 「碎片佔據 div」 的問題 。 就是說, div 裏只要還有一個 內存塊 沒有 歸還, div 就會被 進程 一直佔用, 不能 歸還 操做系統 。 這就致使 大塊內存空間 的 浪費 。 這是一個 大問題 。

有 網友 查了 資料, 說 Linux 有一塊 3G 的 用戶空間, 進程可使用, 使用這個 用戶空間 不須要 系統調用(不須要切換到系統進程, 即不須要跨進程) 。 個人理解是 這是 操做系統 提供的 系統級 的 一個 「公共堆」, 可供全部進程使用 。 這樣在 3G 的範圍內, 進程能夠共用 這個 公共堆, 這樣能夠解決 「碎片 佔據 div」 的 問題 。

因此, 我說 這是個 重大發現 。

但 後來一想 , 這樣 又有一個 問題, 就是 地址訪問 的 時候 不能 或者 難於 做 安全檢測 了, 所謂 安全檢測, 是指 檢查 訪問 的 地址 是否 越界 。 越界 指 訪問了 其它進程 的 內存 。

資料顯示, 如今的 安全檢測 是 在 存儲管理部件 中 完成的 。 這是一個 硬件, 是 CPU 的 一部分 。

操做系統 爲 存儲管理部件 設置 頁表, 而後 存儲管理部件 就能夠工做了 。

看起來, 公共堆 沒有 「段」 的 概念, 大概 很難 實施 判斷 是否越界 的 安全檢查 。

呀, 這可怎麼辦 ?

 

碎片, 分爲 2 個 層面 , 

1  物理內存, 頁文件

2  虛擬內存, 虛擬地址

 

對於 1 , 操做系統 能夠進行整理, 能夠將 多個頁 上的 零碎 的 數據 整理 到 一個 頁, 再把 虛擬地址 映射到 新的頁 就行 。 這樣能夠避免 頻繁的 載入 載出 頁 。

對於 2 , 須要 程序 本身管理 。 好比 GC , 內存池 。

 

但 上面的說法 也有一點問題, 操做系統(虛擬內存) 也不能 整理 數據層面 的 碎片, 由於 虛擬內存 管理的是 虛擬頁 和 物理頁 之間的 對應關係, 並無細化到 虛擬地址 和 物理頁 之間的 對應關係, 因此 虛擬內存 也不能 整理 數據層面 的 碎片, 上面說的 「將 多個頁 上的 零碎 的 數據 整理 到 一個 頁」 這是 不能 作到的 。

操做系統(虛擬內存) 只能 刪除 空頁(沒有數據在用 的 頁) 。

而 只要 頁上還有 數據 在用, 那麼, 即便 數據 佔用的空間 很小, 這個頁也不能被刪除 。

因此, 從這個角度來看, 若是 程序 產生了 不少的 碎片, 那麼可能致使 操做系統(虛擬內存) 頻繁 的 載入載出 頁 。

 

堆 在 計算機系統結構 裏的 地位 等同於 虛擬內存 和 文件系統  。

相關文章
相關標籤/搜索