原文地址:http://blog.csdn.net/jinzhuojun/article/details/13297447程序員
雖然摩爾定律讓咱們的計算機硬件得以以指數速度升級,但反摩爾定律又不斷消減這些升級所帶來的好處。其緣由之一就是面對硬件的更新換代,程序員彷佛不用再對內存「精打細處「了。而近年來隨着穿戴式設備和大數據平臺的興起(一個是內存自己受限,一個是對內存的需求巨大),讓內存的有效利用又成爲了值得開發人員關注的熱點。算法
《Small MemorySoftware: Patterns For System With Limited Memory 》(http://www.smallmemory.com/ , 中文名爲《內存受限系統之軟件開發:針對內存受限系統而整理的模式》)一書探討了編程中有效利用內存的方法。全書分爲五個部分,分別從架構,次級存儲,壓縮,數據結構和內存分配進行了闡述。下面是一些讀書筆記,留做備忘。數據庫
Small Architecture(小內存架構)編程
Memory Limit(Fixed-sized Heap,Memory Partitions):讓系統中的每一個組件負責本身的內存,爲每一個組件設置配額,一旦用完即分配失敗。這種模式主要是爲了防止系統中某一組件耗光系統全部內存的狀況發生,而缺點是容易引發內存浪費,由於會出現系統中一些組件還有空閒內存而另一些內存分配失敗的狀況。實現上主要有三種方法:截獲內存申請釋放操做,獨立堆和獨立進程。瀏覽器
Small Interface:當整個系統被分紅若干個組件,而每一個組件又獨立管理本身的內存使用,那麼當內存數據在這些組件間進行傳遞時,就須要定義接口(或者說協議)。在組件間交換數據有三種方式:Lending(客戶傳對象給系統組件), Borrowing(系統組件傳對象給客戶)和Stealing(可雙向,同時對象全部權也發生變化)。另外組件間的數據傳遞也常常會經過Iterator這種增量式的形式來進行。緩存
Partial Failure(Graceful degradation,Feast and Famine):當系統申請內存失敗時,讓系統在一個「降級模式」下繼續運行而不是直接退出。如字體加載失敗就用系統字體,圖片加載失敗就用佔位符代替。那些由於內存不足而沒法進行的計算能夠不執行或是緩存起來之後執行。當內存不那麼緊張時,系統要能恢復到先前的模式。值得注意的是,處理Partial Failure自己也須要內存,所以須要預分配這部份內存(稱爲Rainy day fund)。後面的Multiple representation和ApplicationSwitching均可做爲其實現形式。數據結構
Captain Oates(Cache Release):當整個系統內存緊張時,砍掉沒必要要或是次要的組件,亦或是清空一些緩存得以讓整個系統繼續運行。它的核心思想是犧牲那些次要的功能來留全那些主要的功能。Captain Oates和Partial Failure中不少技術是通用的,前者描述其它組件內存不足時本組件該幹什麼,後者描述當前組件內存不足時本組件該幹什麼。架構
Read-Only Memory(Use the ROM):有些數據是不常更新的,如可執行代碼,常量字符串,資源文件等。把這些數據放在只讀存儲區域中,至少有兩個好處:其一是能夠經過共享節約內存;另外一好處是要換出到次級存儲時不用寫回操做,從而提升了效率。若是須要更新這些只讀數據能夠用後面會提到的Copy-on-write或者Hooks。app
Hooks(Vector table, Jumptable, Patch table, Interrupt table):想要修改只讀存儲區域上的數據,能夠經過間接訪問只讀內存的方法,中間用位於可寫區域的Hook作跳板。Hook的一個例子就是GOT表,代碼段被加載在只讀段中,而GOT表加載在可寫段中,由於它是要在運行時被Dynamic linker修改的。這樣當代碼段中有外部函數調用時,會經過GOT表來跳轉。這樣就實現了代碼段不變而改變程序邏輯的目的。函數
Secondary Storage (次級存儲)
Application Switching(Phases, ProgramChaining, Command Scripts):假設系統提供不少功能而用戶不會同時用到,就能夠把系統分爲不少個獨立的可執行程序,等用戶須要哪一個再起哪一個,每次只運行一個。這相似於Unix命令的哲學,把一系列功能專注的獨立工具組合鏈接起來完成強大的功能。程序之間可經過參數,磁盤和環境變量等轉遞信息。要傳輸複雜的對象還可能會用到Memento模式。
Data File Pattern(Batch Processing,Filter, Temporary File):若是要處理的數據太多,無法或是不必一次所有載入,就一批批從次級存儲放進內存處理。例如要將一個沒法一次放入內存的大數據排序,能夠先將之切分使之可以載入內存,而後分別進行歸併排序,直到全部數據排序完成。
Resource File Pattern:資源文件通常包括字體,圖標,字符串,佈局和配置信息等。這些東西通常不會被改寫,且在程序運行的任什麼時候候都有可能用到,但用完後便可以被丟到次級存儲器上,下次要用能夠再去磁盤上取。
Packages(Components, LazyLoading, Dynamic Loading, Code Segmentation):將整個系統分紅不少功能包,要什麼功能的時候再動態加載。加載能夠以單獨進程形式也能夠動態連接,亦或手工加載進內存,它們各有利弊。如瀏覽器插件,Java中的class和Linux中的driver都是利用了這種動態加載的概念。實際要注意的是binary級的匹配問題。Abstract Factory模式可用來統一客戶和包實現的接口。Proxy模式可用於包的自動加載和卸載。
Paging Pattern(Virtual Memory,Persistence, Backing Store, Paging OO DBMS):基於空間局部性,將程序最近經常使用的數據(Working set)保留在內存中,其他放在次級存儲器中,要用時再讀進來。它在形式上按粒度從小到大有Demanding page(頁級), Object Oriented Databases(對象級)和Swapping(進程數據級)。典型的例子固然就是操做系統中的頁式管理了。
Compression(壓縮)
Table CompressionPattern(Nibble Coding, HuffmanCoding):因爲數據中每一個元素出現機率不同(信息量不同)或者說重要性程度不同,所以若是對原始數據進行從新編碼,能夠達到壓縮的目的。典型的例子如Huffman編碼(基於字符出現頻率不同)和JPEG(基於人眼對色彩的敏感程度不同)。
Difference CodingPattern(Delta Coding, RunLength Encoding):差分編碼,即利用數據(尤爲是音、視頻等流數據)的先後相關性進行壓縮編碼,如MPEG格式。咱們知道壓縮視頻中通常有關鍵幀和非關鍵幀之分,關鍵幀可被獨立解碼,而非關鍵幀只存有差分信息。差分編碼的實現技術有Delta Coding,Run Length Encoding,LossyDifference Compression,此外還需爲傳錯而考慮重同步問題。
Adaptive CompressionPattern:在壓縮前或者壓縮中分析數據來獲得最好的壓縮參數或實時調整參數。與前兩種方式相比自適應算法處理時間更長,所以不適用於實時任務。如gzip, bzip2壓縮方法就使用了該模式。
Small Data Structure(小內存數據結構)
Packed Data(Bit Packing):一方面咱們有時會用過大的類型存儲數據,另外一方面計算機體系通常會將數據進行對齊存儲從而優化讀寫效率,有時這會致使內存利用效率的下降。咱們能夠經過將數據結構進行pack來減小內存的使用。缺點是它會較大地影響性能。注意將用句柄代替指針也能節省一部份內存,由於句柄通常都比指針小,能夠用更小的數據類型存儲。
Sharing (Normalisation):內存共享,節約內存。共享的東西在內存中僅須要一份拷貝。有些是明顯能夠共享的,就像動態庫libc.so,有些因爲內容重複能夠共享的,如Java中的String使用了Flyweight模式,對重複字符串只存儲一份。簡單的共享能夠用靜態全局變量,Sigleton模式或者LazyInitialization實現。複雜點的能夠用Shared cache,即把全部的共享對象放入數據庫(cache),使用時用key查詢來獲得。
Copy-on-Write:共享的東西須要改動時,就將之拷貝一份,再進行讀寫。像Linux中對於共享頁在頁表中是標識成只讀的,當對其進行寫操做時,就會發生page fault,從而把控制權交還kernel進行Copy-on-write的處理。Copy-on-write經常使用Proxy模式實現,以實現過程對客戶透明。
Embedded Pointer:對於一些用指針組建的數據結構(如鏈表,樹,圖等),將先後指針放在要存儲的目標對象中,不只能節省維護數據結構自己的內存,並且能在遍歷時節省臨時內存(如棧,隊列)的使用。實現起來有三種方式:繼承,內聯和預處理。像Linux中的list_head就是用了內聯嵌入指針。另外相關的技術還有Pointer Differences和Pointer reversal。
Multiple Representation:和Template Method及Strategy模式的做用差很少,只是這兒用在內存利用上了。即準備多套實現,根據內存的使用狀況進行切換。該模式一大優勢是對客戶透明。例子如Java和STL中的容器類,它們接口統一,但對內存的使用策略卻不一樣。
Memory Allocation(內存分配)
原則上只要能知足需求,能用簡單的就用簡單的。內存分配主要就是和兩個問題做鬥爭:一是碎片問題(解決方法有固定分配,拷貝壓縮,池式管理等);二是內存不足問題(解決方法有固定客戶內存大小,產生錯誤信號,下降質量,刪除舊對象,延遲申請和忽略問題等)。
Fixed Allocation(Static Allocation,Pre-allocation):初始化時分配固定大小內存,程序執行時再也不動態分配,而初始化時能夠動態也能夠靜態分配。這樣作的好處是不會出現分配失敗的狀況,系統可預測性好,且內存分配高效,壞處是不夠靈活。
Variable Allocation(Dynamic Allocation):當須要的時候再分配內存,適合預先不知道數據大小的狀況(如通用庫)。它的缺點也是Fixed Allocation的優勢:即分配可能失敗(此時能夠考慮引入Partial Failure),分配操做會影響性能(將Fixed Allocation與之結合,就有了Pooled Allocation),須要本身釋放內存(可用Referencecounting和Garbage Collection),另外動態分配還更容易產生碎片(可用Compaction壓縮已有對象,MemoryDiscard刪除臨時對象)。
Memory Discard(Stack Allocation,Scratchpad):適用於臨時用的內存。在操做前從棧、堆或是預分配區域中進行分配,操做完了就一塊兒丟棄。好處是內存的分配和釋放都只須要移動下指針便可,簡單粗暴高效。函數調用過程當中棧幀的處理就是該模式的例子。另外一種少見的狀況就是要用一個現成的但有內存泄露問題的組件,能夠在加載它以前爲之分配臨時內存,卸載時把它的內存所有統一清空。用Memory Discard的時候要格外當心臨時對象所持有的外部資源引用,在釋放內存前要先釋放這些資源。
Pooled Allocation(Memory Pool):內存池適用於大小相似,但次數較爲頻繁的內存分配請求。一般作法是先預分配一塊內存(經過Fixed Allocation),將其按固定大小切分紅爲內存池,用空閒隊列進行管理。程序執行過程當中須要內存了就從裏邊拿,釋放後也放回其中(Variable Allocation)。該模式結合了Fixed Allocation和VariableAllocation的優勢。Linux中的Slab就是個例子。
Compaction(Manged Table, MemoryHandles, Defragmentation):壓縮主要用來處理Variable Allocation產生的碎片問題。當系統跑了很長一段時間後,剩下不少不連續的空閒內存,當用戶申請一塊大的內存時,申請就容易失敗。比較簡單的方法就是拷貝數據讓它們整一起去。注意因爲指針的存在,這些指針在壓縮過程當中須要被及時更新,該問題通常可用間接引用(即用句柄或對象表代替指針)來解決。
Reference Counting:主要用於共享對象的回收,使這些對象在沒人使用時被自動回收。大致思路是在對象中記錄該對象的引用數來判斷其是不是垃圾,當引用數爲0時說明無人引用,能夠被清除。和GarbageCollection相比,它的優勢是自己的overhead被分攤到整個執行過程,所以對用戶的實時響應影響不大,且一旦對象變垃圾當即會被清除;缺點是須要程序員來主動維護引用計數,且影響系統性能,另外要處理環形引用比較麻煩。
Garbage Collection(Mark-sweep GarbageCollection, Tracing Garbage Collection):也是主要用來處理共享對象的回收。系統在必定時候(通常是現有內存不足時)觸發垃圾回收,而後從一些根結點(一般爲棧變量,全局及靜態變量,外部庫的引用等)出發遍歷全部被引用對象。這樣剩下的就是能夠被清除的垃圾了。清除垃圾有兩種作法:Mark-sweep GC和Copying GC,實際中它們可結合使用(像mono中的SGen)。Garbage Collection的優勢是更加自動,無需用戶參於,且整體性能損失相對更小;缺點是回收時會有停頓現象,另外對Finalization(通常用於釋放文件句柄或設備等外部資源)的支持會比較麻煩(通常作法是把它們加入到待辦隊列,而後到單獨線程去調用,但這樣它們被調用的時間就很差預測,容易使系統變得不穩定)。因爲該模式和Reference Counting各有利弊,有些時候系統中它們會同時出現。