詳細設計存儲分配
❶ 分區存儲管理中常用哪些分配策略
1、固定分區存儲管理
其基本思想是將內存劃分成若干固定大小的分區,每個分區中最多隻能裝入一個作業。當作業申請內存時,系統按一定的演算法為其選擇一個適當的分區,並裝入內存運行。由於分區大小是事先固定的,因而可容納作業的大小受到限制,而且當用戶作業的地址空間小於分區的存儲空間時,造成存儲空間浪費。
一、空間的分配與回收
系統設置一張「分區分配表」來描述各分區的使用情況,登記的內容應包括:分區號、起始地址、長度和佔用標志。其中佔用標志為「0」時,表示目前該分區空閑;否則登記佔用作業名(或作業號)。有了「分區分配表」,空間分配與回收工作是比較簡單的。
二、地址轉換和存儲保護
固定分區管理可以採用靜態重定位方式進行地址映射。
為了實現存儲保護,處理器設置了一對「下限寄存器」和「上限寄存器」。當一個已經被裝入主存儲器的作業能夠得到處理器運行時,進程調度應記錄當前運行作業所在的分區號,且把該分區的下限地址和上限地址分別送入下限寄存器和上限寄存器中。處理器執行該作業的指令時必須核對其要訪問的絕對地址是否越界。
三、多作業隊列的固定分區管理
為避免小作業被分配到大的分區中造成空間的浪費,可採用多作業隊列的方法。即系統按分區數設置多個作業隊列,將作業按其大小排到不同的隊列中,一個隊列對應某一個分區,以提高內存利用率。
2、可變分區存儲管理
可變分區存儲管理不是預先將內存劃分分區,而是在作業裝入內存時建立分區,使分區的大小正好與作業要求的存儲空間相等。這種處理方式使內存分配有較大的靈活性,也提高了內存利用率。但是隨著對內存不斷地分配、釋放操作會引起存儲碎片的產生。
一、空間的分配與回收
採用可變分區存儲管理,系統中的分區個數與分區的大小都在不斷地變化,系統利用「空閑區表」來管理內存中的空閑分區,其中登記空閑區的起始地址、長度和狀態。當有作業要進入內存時,在「空閑區表」中查找狀態為「未分配」且長度大於或等於作業的空閑分區分配給作業,並做適當調整;當一個作業運行完成時,應將該作業佔用的空間作為空閑區歸還給系統。
可以採用首先適應演算法、最佳(優)適應演算法和最壞適應演算法三種分配策略之一進行內存分配。
二、地址轉換和存儲保護
可變分區存儲管理一般採用動態重定位的方式,為實現地址重定位和存儲保護,系統設置相應的硬體:基址/限長寄存器(或上界/下界寄存器)、加法器、比較線路等。
基址寄存器用來存放程序在內存的起始地址,限長寄存器用來存放程序的長度。處理機在執行時,用程序中的相對地址加上基址寄存器中的基地址,形成一個絕對地址,並將相對地址與限長寄存器進行計算比較,檢查是否發生地址越界。
三、存儲碎片與程序的移動
所謂碎片是指內存中出現的一些零散的小空閑區域。由於碎片都很小,無法再利用。如果內存中碎片很多,將會造成嚴重的存儲資源浪費。解決碎片的方法是移動所有的佔用區域,使所有的空閑區合並成一片連續區域,這一技術稱為移動技術(緊湊技術)。移動技術除了可解決碎片問題還使內存中的作業進行擴充。顯然,移動帶來系統開銷加大,並且當一個作業如果正與外設進行I/O時,該作業是無法移動的。
3、頁式存儲管理
基本原理
1.等分內存
頁式存儲管理將內存空間劃分成等長的若干區域,每個區域的大小一般取2的整數冪,稱為一個物理頁面有時稱為塊。內存的所有物理頁面從0開始編號,稱作物理頁號。
2.邏輯地址
系統將程序的邏輯空間按照同樣大小也劃分成若干頁面,稱為邏輯頁面也稱為頁。程序的各個邏輯頁面從0開始依次編號,稱作邏輯頁號或相對頁號。每個頁面內從0開始編址,稱為頁內地址。程序中的邏輯地址由兩部分組成:
邏輯地址
頁號p
頁內地址 d
3.內存分配
系統可用一張「位示圖」來登記內存中各塊的分配情況,存儲分配時以頁面(塊)為單位,並按程序的頁數多少進行分配。相鄰的頁面在內存中不一定相鄰,即分配給程序的內存塊之間不一定連續。
對程序地址空間的分頁是系統自動進行的,即對用戶是透明的。由於頁面尺寸為2的整數次冪,故相對地址中的高位部分即為頁號,低位部分為頁內地址。
3.5.2實現原理
1.頁表
系統為每個進程建立一張頁表,用於記錄進程邏輯頁面與內存物理頁面之間的對應關系。地址空間有多少頁,該頁表裡就登記多少行,且按邏輯頁的順序排列,形如:
邏輯頁號
主存塊號
0
B0
1
B1
2
B2
3
B3
2.地址映射過程
頁式存儲管理採用動態重定位,即在程序的執行過程中完成地址轉換。處理器每執行一條指令,就將指令中的邏輯地址(p,d)取來從中得到邏輯頁號(p),硬體機構按此頁號查頁表,得到內存的塊號B』,便形成絕對地址(B』,d),處理器即按此地址訪問主存。
3.頁面的共享與保護
當多個不同進程中需要有相同頁面信息時,可以在主存中只保留一個副本,只要讓這些進程各自的有關項中指向內存同一塊號即可。同時在頁表中設置相應的「存取許可權」,對不同進程的訪問許可權進行各種必要的限制。
4、段式存儲管理
基本原理
1.邏輯地址空間
程序按邏輯上有完整意義的段來劃分,稱為邏輯段。例如主程序、子程序、數據等都可各成一段。將一個程序的所有邏輯段從0開始編號,稱為段號。每一個邏輯段都是從0開始編址,稱為段內地址。
2.邏輯地址
程序中的邏輯地址由段號和段內地址(s,d)兩部分組成。
3.內存分配
系統不進行預先劃分,而是以段為單位進行內存分配,為每一個邏輯段分配一個連續的內存區(物理段)。邏輯上連續的段在內存不一定連續存放。
3.6.2實現方法
1.段表
系統為每個進程建立一張段表,用於記錄進程的邏輯段與內存物理段之間的對應關系,至少應包括邏輯段號、物理段首地址和該段長度三項內容。
2.建立空閑區表
系統中設立一張內存空閑區表,記錄內存中空閑區域情況,用於段的分配和回收內存。
3.地址映射過程
段式存儲管理採用動態重定位,處理器每執行一條指令,就將指令中的邏輯地址(s,d)取來從中得到邏輯段號(s),硬體機構按此段號查段表,得到該段在內存的首地址S』, 該段在內存的首地址S』加上段內地址d,便形成絕對地址(S』+d),處理器即按此地址訪問主存。
5、段頁式存儲管理
頁式存儲管理的特徵是等分內存,解決了碎片問題;段式存儲管理的特徵是邏輯分段,便於實現共享。為了保持頁式和段式上的優點,結合兩種存儲管理方案,形成了段頁式存儲管理。
段頁式存儲管理的基本思想是:把內存劃分為大小相等的頁面;將程序按其邏輯關系劃分為若干段;再按照頁面的大小,把每一段劃分成若干頁面。程序的邏輯地址由三部分組成,形式如下:
邏輯地址
段號s
頁號p
頁內地址d
內存是以頁為基本單位分配給每個程序的,在邏輯上相鄰的頁面內存不一定相鄰。
系統為每個進程建立一張段表,為進程的每一段各建立一張頁表。地址轉換過程,要經過查段表、頁表後才能得到最終的物理地址。
❷ plc中如何進行存儲分配
雖然各種PLC的CPU的最大定址空間各不相同,但是根據PLC的工作原理,其存儲空間一般包括以下三個區域:
(1)系統程序存儲區
(2)系統RAM存儲區(包括I/O映象區和系統軟設備等)
(3)用戶程序存儲區系統程序存儲區:在系統程序存儲區中存放著相當於計算機操作系統的系統程序。包括監控程序、管理程序、命令解釋程序、功能子程序、系統診斷子程序等。由製造廠商將其固化在EPROM中,用戶不能直接存取。它和硬體一起決定了該PLC的性能。
系統RAM存儲區:系統RAM存儲區包括I/O映象區以及各類軟設備,如:邏輯線圈;數據寄存器;計時器;計數器;變址寄存器;累加器等存儲器。
(1)I/O映象區:由於PLC投入運行後,只是在輸入采樣階段才依次讀入各輸入狀態和數據,在輸出刷新階段才將輸出的狀態和數據送至相應的外設。因此,它需要一定數量的存儲單元(RAM)以存放I/O的狀態和數據,這些單元稱作I/O映象區。一個開關量I/O佔用存儲單元中的一個位(bit),一個模擬量I/O佔用存儲單元中的一個字(16個bit)。因此整個I/O映象區可看作兩個部分組成:開關量I/O映象區;模擬量I/O映象區。
(2)系統軟設備存儲區 :除了I/O映象區區以外,系統RAM存儲區還包括PLC內部各類軟設備(邏輯線圈、計時器、計數器、數據寄存器和累加器等)的存儲區。該存儲區又分為具有失電保持的存儲區域和無失電保持的存儲區域,前者在PLC斷電時,由內部的鋰電池供電,數據不會遺失;後者當PLC斷電時,數據被清零。
1)邏輯線圈與開關輸出一樣,每個邏輯線圈佔用系統RAM存儲區中的一個位,但不能直接驅動外設,只供用戶在編程中使用,其作用類似於電器控制線路中的繼電器。另外,不同的PLC還提供數量不等的特殊邏輯線圈,具有不同的功能。
2)數據寄存器與模擬量I/O一樣,每個數據寄存器佔用系統RAM存儲區中的一個字(16 bits)。 另外,PLC還提供數量不等的特殊數據寄存器,具有不同的功能。
3)計時器
4)計數器
用戶程序存儲區
用戶程序存儲區存放用戶編制的用戶程序。不同類型的PLC,其存儲容量各不相同。
PLC的電源在整個系統中起著十分重要得作用。如果沒有一個良好的、可*得電源系統是無法正常工作的,因此PLC的製造商對電源的設計和製造也十分重視。
❸ 存儲空間的分配技術有哪些
存儲空間的分配技術可以分為以下幾種:
靜態分配:靜態分配是指在計算機系統中為程序或數據分配一定的存儲空間,分配的空間在程序執行期間不變。靜態分配的優點是簡單、快速,但缺點是空間利用率低,容易余兄出現空間浪費或不足的情況。
動態分配:動態分配是指在程序運行期間根據需要動態地為程序或數據分配存儲空間。動態分配的優點是可以充分利用存儲空間,避免空間浪費或不足的情況,但缺點是分配和釋放空間的過程比較復雜,容易出現內存泄漏或空間碎片等問題。
分頁式分配:分頁式分配是指將存儲空間劃分為固定大小的頁面,並將程序寬仔或數據按照頁面進行分配。這種技術可以充分利用存儲空間,避免空間浪費,也可以避免空間碎片的問題,但需要花費一定的開銷來維護頁面表等數據結構。
段式分配:段式分配是指將存儲空間按照邏輯上的段進行劃分,並為程序或數據分配不同大小的段空間。這種技術可以更好地滿足不同程序或數據的存儲需求,但需要考慮內存碎片和段的大小等問題。
段頁式分配:段頁式分配是指將存儲空間先按照段進行劃分,再將每個段劃分為固定大小的頁面,並將程序或數據按照段和頁面進行分配。這種技術可以更好地結合段式和分頁式分配的優點,但需要處理復雜的地址映射和內存管理問題慎毀汪。
不同的存儲空間分配技術適用於不同的計算機應用和場景,需要根據具體的需求和實際情況進行選擇和優化。
❹ 在分頁存儲管理方式下應怎樣實現主存空間的分配和回收
2.1 模擬包括3部分:
1)實現特定的內存分配演算法
2)實現內存回收模擬
3)每種內存分配策略對應的碎片數統計
2.2 固定分區存儲管理
假設內存容量為120KB,並且分別劃分成8,16,32,64KB大小的塊各一塊。
一個進程所需要的內存為0到100個KB。同時假設一個進程在運行過程中所需內存的大小不變。
模擬五個進程到達請求分配與運行完回收情況,輸出主存分配表.
2.3 動態分區分配存儲管理
採用連續分配方式之動態分區分配存儲管理,使用首次適應演算法、下次適應演算法、最佳適應演算法和最壞適應演算法4種演算法完成設計(任選兩種演算法)。
❺ 操作系統-04-操作系統的存儲管理和設備管理
早期的計算機由於結構較為簡單,存儲容量小,並不需要過多的的存儲管理。
隨著計算機和程序越來越復雜,使得存儲管理成為必要。
單一連續分配是最簡單的內存分配方式
只能在單用戶、單進程的操作系統中使用
固定分區分配是支持多道程序的最簡單存儲分配方式
內存空間被劃分為若干固定大小的區域
每個分區只提供給一個程序使用,互不幹擾
根據進程實際需要,動態分配內存空間
不需要新建空閑鏈表節點
只需要把空閑區的容量增大為包括回收區的容量即可
將回收區和空閑區合並
新的空閑區使用原來回收區的地址
將兩個空閑區和中間的回收區合並
新的空閑區使用空閑區1的地址
為回收區創建新的空閑節點
將該節點插入到相應的空閑區鏈表中
上面的部分主要是從物理的角度講解內存管理,這部分主要是講解操作系統是怎麼管理進程的內存空間。
字塊 是相對於物理設備的定義, 頁面 是相對邏輯空間的定義。
頁式存儲管理主要是將進程邏輯空間等分成若干大小的頁面,相應的把物理內存空間分成與頁面大小的物理塊,以頁面為單位把進程空間裝進物理內存中分散的物理塊。
頁面大小應該適中,過大難以分配,過小內存碎片過多,通常是512B~8K。
頁表 記錄進程邏輯空間於物理空間的映射
在頁式存儲管理, 頁地址 = 頁號 + 頁內偏移
現代計算機系統中,可以支持非常大的邏輯 地址空間(2 32~2 64),這樣,頁表就 變得非常大,要佔用非常大的內存空間,如, 具有32位邏輯地址空間的分頁系統,規定頁 面大小為4KB,則在每個進程頁表中的頁表 項可達1M(2^20)個,如果每個頁表項佔用 1Byte,故每個進程僅僅頁表就要佔用1MB 的內存空間。
為了解決這個問題,引入了多級頁表。
多級頁表有一個根頁表,每一個字塊指向了內存中的一片空間,這塊空間存儲的是二級頁表。以此類推,最後一級頁表指向的字塊才是進程實際使用的內存。通過這種分級機制,大大減少了進程中頁表數佔用的空間。
段式存儲管理將進程邏輯空間劃分成若干段(非等分),段的長度由連續邏輯的長度決定。
例如一個程序有主函數MAIN、子程序段X、子函數Y等,這個時候會根據每一個函數的邏輯長度來分配邏輯空間。
頁表由 頁號 和 基址 組成,但在段式存儲管理中由於每一段的長度是不固定的,段表由 段號 、 基址 以及 段長 組成。
在段式存儲管理, 段地址 = 段號 + 段內偏移
分頁可以有效提高內存利用率(雖然說存在頁內碎片)
分段可以更好滿足用戶需求
兩者結合,形成段頁式存儲管理
先將邏輯空間按段式管理分成若干段,再把段內空間按頁式管理等分成若干頁。
在段頁式存儲管理中, 段頁地址 = 段號 + 段內頁號 + 頁內地址
有些進程實際需要的內存很大,超過物理內存的容量。
由於操作系統的多道程序設計,使得每個進程可用物理內存更加稀缺。
不可能無限增加物理內存,物理內存總有不夠的時候,於是便有了虛擬內存的概念。
虛擬內存是操作系統內存管理的關鍵技術,使得多道程序運行和大程序運行成為現實,她通過將進程所使用的內存進行劃分,將部分暫時不使用的內存放置在輔存。
根據局部性原理,程序運行時,無需全部裝入內存,裝載部分即可。如果訪問頁不在內存,則發出缺頁中斷,發起頁面置換。
從用戶層面看,程序擁有很大的空間,即是虛擬內存。
虛擬內存實際是對物理內存的補充,速度接近於內存,成本接近於輔存。
置換演算法一般有先進先出演算法(FIFO)、最不經常使用演算法(LFU)、最近最少使用演算法(LRU)。
從計算機組成原理篇章中,我們可以知道,CPU的高速緩存沒有數據時,需要從主存中載入數據。此時若主存中也沒有數據,則需要從輔存中載入頁面數據。
內存替換策略發生在Cache-主存層次、主存-輔存層次。Cache-主存層次的替換策略主要是為了解決 速度問題 ,
主存-輔存層次則。主要是為了解決 容量問題 。
順序文件是指按順序存放在存儲介質中的文件,例如磁帶的存儲特性使得磁帶文件只能存儲順序文件。
順序文件是所有邏輯文件當中存儲效率最高的。
可變長文件不適合使用順序文件格式存儲,索引文件是為了解決可變長文件存儲而發明的一種文件格式,索引文件需要配合索引表完成存儲的操作。
目錄的層級結構是樹狀的,成為目錄樹。
目錄樹中任何文件或目錄都只有唯一路徑。
對CPU而言,凡是對CPU進行數據輸入的都是輸入設備,凡是CPU進行數據輸出的都是輸出設備。
緩沖區主要是解決CPU與IO設備的速率不匹配的問題,減少CPU處理IO請求的頻率,提高CPU與IO設備之間的並行性。
專用緩沖區只適用於特定的IO進程,當這樣的IO進程比較多時,對內存的消耗也很大,所以操作系統劃出可供多個進程使用的公共緩沖區,稱之為緩沖池。
SPOOLing技術是關於慢速字元設備如何與計算機主機交換信息的一種技術,利用高速共享設備將低速的獨享設備模擬為高速的共享設備,邏輯上,系統為每一個用戶都分配了一台獨立的高速獨享設備,是一種虛擬設備技術。
SPOOLing技術把同步調用低速設備改為非同步調用。在輸入、輸出之間增加了排隊轉儲環節(輸入井、輸出井),SPOOLing負責輸入(出)井與低速設備之間的調度,邏輯上,進程直接與高速設備交互,減少了進程的等待時間。
❻ 常用的內存管理方法有哪幾種
常用的內存管理 方法 有哪幾種?下面是我給大家收集整理的一些相關方法技巧,希望對大家有幫助!
常用的內存管理方法傳統的內存整理軟體工作原理大概是:先申請一塊“巨大內存”。因為物理內存幾乎全被內存整理軟體佔用,因此Windows被迫把其他軟體的內存數據轉移到硬碟上的“虛擬內存交換文件”(PageFile)中,完成這一過程之後內存整理軟體就會釋放掉剛剛申請的內存,至此整理過程完成,可用物理內存顯著增加。
大體上都是那麼回事,就是通過輔助空間,重新安排內存內容 ....
但是其中使用的演算法,效率是有很大的區別的 ~~ <script type="text/javascript"><!-- google_ad_client = "pub-4403405132739389"; google_ad_width = 250; google_ad_height = 250; google_ad_format = "250x250_as"; google_ad_type = "text"; //2007-10-22: 250*250 google_ad_channel = "7687946060"; google_ui_features = "rc:10"; //--> </script><script type="text/javascript" src=pagead2.googlesyndication/pagead/show_ads.js"> </script>
拓荒時代
國內的程序員大多是在 Java 語言中第一次感受到垃圾收集技術的巨大魅力的,許多人也因此把 Java 和垃圾收集看成了密不可分的整體。但事實上,垃圾收集技術早在 Java 語言問世前 30 多年就已經發展和成熟起來了, Java 語言所做的不過是把這項神奇的技術帶到了廣大程序員身邊而已。
如果一定要為垃圾收集技術找一個孿生兄弟,那麼, Lisp 語言才是當之無愧的人選。 1960 年前後誕生於 MIT 的 Lisp 語言是第一種高度依賴於動態內存分配技術的語言: Lisp 中幾乎所有數據都以“表”的形式出現,而“表”所佔用的空間則是在堆中動態分配得到的。 Lisp 語言先天就具有的動態內存管理特性要求 Lisp 語言的設計者必須解決堆中每一個內存塊的自動釋放問題(否則, Lisp 程序員就必然被程序中不計其數的 free 或 delete 語句淹沒),這直接導致了垃圾收集技術的誕生和發展——說句題外話,上大學時,一位老師曾告訴我們, Lisp 是對現代軟體開發技術貢獻最大的語言。我當時對這一說法不以為然:布滿了圓括弧,看上去像迷宮一樣的 Lisp 語言怎麼能比 C 語言或 Pascal 語言更偉大呢?不過現在,當我知道垃圾收集技術、數據結構技術、人工智慧技術、並行處理技術、虛擬機技術、元數據技術以及程序員們耳熟能詳的許多技術都起源於 Lisp 語言時,我特別想向那位老師當面道歉,並收回我當時的幼稚想法。
知道了 Lisp 語言與垃圾收集的密切關系,我們就不難理解,為什麼垃圾收集技術的兩位先驅者 J. McCarthy 和 M. L. Minsky 同時也是 Lisp 語言發展史上的重要人物了。 J. McCarthy 是 Lisp 之父,他在發明 Lisp 語言的同時也第一次完整地描述了垃圾收集的演算法和實現方式; M. L. Minsky 則在發展 Lisp 語言的過程中成為了今天好幾種主流垃圾收集演算法的奠基人——和當時不少技術大師的經歷相似, J. McCarthy 和 M. L. Minsky 在許多不同的技術領域里都取得了令人艷羨的成就。也許,在 1960 年代那個軟體開發史上的拓荒時代里,思維敏捷、意志堅定的研究者更容易成為無所不能的西部硬漢吧。
在了解垃圾收集演算法的起源之前,有必要先回顧一下內存分配的主要方式。我們知道,大多數主流的語言或運行環境都支持三種最基本的內存分配方式,它們分別是:
一、靜態分配( Static Allocation ):靜態變數和全局變數的分配形式。我們可以把靜態分配的內存看成是家裡的耐用傢具。通常,它們無需釋放和回收,因為沒人會天天把大衣櫃當作垃圾扔到窗外。
二、自動分配( Automatic Allocation ):在棧中為局部變數分配內存的方法。棧中的內存可以隨著代碼塊退出時的出棧操作被自動釋放。這類似於到家中串門的訪客,天色一晚就要各回各家,除了個別不識時務者以外,我們一般沒必要把客人捆在垃圾袋裡掃地出門。
三、動態分配( Dynamic Allocation ):在堆中動態分配內存空間以存儲數據的方式。堆中的內存塊好像我們日常使用的餐巾紙,用過了就得扔到垃圾箱里,否則屋內就會滿地狼藉。像我這樣的懶人做夢都想有一台家用機器人跟在身邊打掃衛生。在軟體開發中,如果你懶得釋放內存,那麼你也需要一台類似的機器人——這其實就是一個由特定演算法實現的垃圾收集器。
也就是說,下面提到的所有垃圾收集演算法都是在程序運行過程中收集並清理廢舊“餐巾紙”的演算法,它們的操作對象既不是靜態變數,也不是局部變數,而是堆中所有已分配內存塊。
引用計數( Reference Counting )演算法
1960 年以前,人們為胚胎中的 Lisp 語言設計垃圾收集機制時,第一個想到的演算法是引用計數演算法。拿餐巾紙的例子來說,這種演算法的原理大致可以描述為:
午餐時,為了把腦子里突然跳出來的設計靈感記下來,我從餐巾紙袋中抽出一張餐巾紙,打算在上面畫出系統架構的藍圖。按照“餐巾紙使用規約之引用計數版”的要求,畫圖之前,我必須先在餐巾紙的一角寫上計數值 1 ,以表示我在使用這張餐巾紙。這時,如果你也想看看我畫的藍圖,那你就要把餐巾紙上的計數值加 1 ,將它改為 2 ,這表明目前有 2 個人在同時使用這張餐巾紙(當然,我是不會允許你用這張餐巾紙來擦鼻涕的)。你看完後,必須把計數值減 1 ,表明你對該餐巾紙的使用已經結束。同樣,當我將餐巾紙上的內容全部謄寫到 筆記本 上之後,我也會自覺地把餐巾紙上的計數值減 1 。此時,不出意外的話,這張餐巾紙上的計數值應當是 0 ,它會被垃圾收集器——假設那是一個專門負責打掃衛生的機器人——撿起來扔到垃圾箱里,因為垃圾收集器的惟一使命就是找到所有計數值為 0 的餐巾紙並清理它們。
引用計數演算法的優點和缺陷同樣明顯。這一演算法在執行垃圾收集任務時速度較快,但演算法對程序中每一次內存分配和指針操作提出了額外的要求(增加或減少內存塊的引用計數)。更重要的是,引用計數演算法無法正確釋放循環引用的內存塊,對此, D. Hillis 有一段風趣而精闢的論述:
一天,一個學生走到 Moon 面前說:“我知道如何設計一個更好的垃圾收集器了。我們必須記錄指向每個結點的指針數目。” Moon 耐心地給這位學生講了下面這個 故事 :“一天,一個學生走到 Moon 面前說:‘我知道如何設計一個更好的垃圾收集器了……’”
D. Hillis 的故事和我們小時候常說的“從前有座山,山上有個廟,廟里有個老和尚”的故事有異曲同工之妙。這說明,單是使用引用計數演算法還不足以解決垃圾收集中的所有問題。正因為如此,引用計數演算法也常常被研究者們排除在狹義的垃圾收集演算法之外。當然,作為一種最簡單、最直觀的解決方案,引用計數演算法本身具有其不可替代的優越性。 1980 年代前後, D. P. Friedman , D. S. Wise , H. G. Baker 等人對引用計數演算法進行了數次改進,這些改進使得引用計數演算法及其變種(如延遲計數演算法等)在簡單的環境下,或是在一些綜合了多種演算法的現代垃圾收集系統中仍然可以一展身手。
標記-清除( Mark-Sweep )演算法
第一種實用和完善的垃圾收集演算法是 J. McCarthy 等人在 1960 年提出並成功地應用於 Lisp 語言的標記-清除演算法。仍以餐巾紙為例,標記-清除演算法的執行過程是這樣的:
午餐過程中,餐廳里的所有人都根據自己的需要取用餐巾紙。當垃圾收集機器人想收集廢舊餐巾紙的時候,它會讓所有用餐的人先停下來,然後,依次詢問餐廳里的每一個人:“你正在用餐巾紙嗎?你用的是哪一張餐巾紙?”機器人根據每個人的回答將人們正在使用的餐巾紙畫上記號。詢問過程結束後,機器人在餐廳里尋找所有散落在餐桌上且沒有記號的餐巾紙(這些顯然都是用過的廢舊餐巾紙),把它們統統扔到垃圾箱里。
正如其名稱所暗示的那樣,標記-清除演算法的執行過程分為“標記”和“清除”兩大階段。這種分步執行的思路奠定了現代垃圾收集演算法的思想基礎。與引用計數演算法不同的是,標記-清除演算法不需要運行環境監測每一次內存分配和指針操作,而只要在“標記”階段中跟蹤每一個指針變數的指向——用類似思路實現的垃圾收集器也常被後人統稱為跟蹤收集器( Tracing Collector )
伴隨著 Lisp 語言的成功,標記-清除演算法也在大多數早期的 Lisp 運行環境中大放異彩。盡管最初版本的標記-清除演算法在今天看來還存在效率不高(標記和清除是兩個相當耗時的過程)等諸多缺陷,但在後面的討論中,我們可以看到,幾乎所有現代垃圾收集演算法都是標記-清除思想的延續,僅此一點, J. McCarthy 等人在垃圾收集技術方面的貢獻就絲毫不亞於他們在 Lisp 語言上的成就了。
復制( Copying )演算法
為了解決標記-清除演算法在垃圾收集效率方面的缺陷, M. L. Minsky 於 1963 年發表了著名的論文“一種使用雙存儲區的 Lisp 語言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage )”。 M. L. Minsky 在該論文中描述的演算法被人們稱為復制演算法,它也被 M. L. Minsky 本人成功地引入到了 Lisp 語言的一個實現版本中。
復制演算法別出心裁地將堆空間一分為二,並使用簡單的復制操作來完成垃圾收集工作,這個思路相當有趣。借用餐巾紙的比喻,我們可以這樣理解 M. L. Minsky 的復制演算法:
餐廳被垃圾收集機器人分成南區和北區兩個大小完全相同的部分。午餐時,所有人都先在南區用餐(因為空間有限,用餐人數自然也將減少一半),用餐時可以隨意使用餐巾紙。當垃圾收集機器人認為有必要回收廢舊餐巾紙時,它會要求所有用餐者以最快的速度從南區轉移到北區,同時隨身攜帶自己正在使用的餐巾紙。等所有人都轉移到北區之後,垃圾收集機器人只要簡單地把南區中所有散落的餐巾紙扔進垃圾箱就算完成任務了。下一次垃圾收集的工作過程也大致類似,惟一的不同只是人們的轉移方向變成了從北區到南區。如此循環往復,每次垃圾收集都只需簡單地轉移(也就是復制)一次,垃圾收集速度無與倫比——當然,對於用餐者往返奔波於南北兩區之間的辛勞,垃圾收集機器人是決不會流露出絲毫憐憫的。
M. L. Minsky 的發明絕對算得上一種奇思妙想。分區、復制的思路不僅大幅提高了垃圾收集的效率,而且也將原本繁紛復雜的內存分配演算法變得前所未有地簡明和扼要(既然每次內存回收都是對整個半區的回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存就可以了),這簡直是個奇跡!不過,任何奇跡的出現都有一定的代價,在垃圾收集技術中,復制演算法提高效率的代價是人為地將可用內存縮小了一半。實話實說,這個代價未免也太高了一些。
無論優缺點如何,復制演算法在實踐中都獲得了可以與標記-清除演算法相比擬的成功。除了 M. L. Minsky 本人在 Lisp 語言中的工作以外,從 1960 年代末到 1970 年代初, R. R. Fenichel 和 J. C. Yochelson 等人也相繼在 Lisp 語言的不同實現中對復制演算法進行了改進, S. Arnborg 更是成功地將復制演算法應用到了 Simula 語言中。
至此,垃圾收集技術的三大傳統演算法——引用計數演算法、標記-清除演算法和復制演算法——都已在 1960 年前後相繼問世,三種演算法各有所長,也都存在致命的缺陷。從 1960 年代後期開始,研究者的主要精力逐漸轉向對這三種傳統演算法進行改進或整合,以揚長避短,適應程序設計語言和運行環境對垃圾收集的效率和實時性所提出的更高要求。
走向成熟
從 1970 年代開始,隨著科學研究和應用實踐的不斷深入,人們逐漸意識到,一個理想的垃圾收集器不應在運行時導致應用程序的暫停,不應額外佔用大量的內存空間和 CPU 資源,而三種傳統的垃圾收集演算法都無法滿足這些要求。人們必須提出更新的演算法或思路,以解決實踐中碰到的諸多難題。當時,研究者的努力目標包括:
第一,提高垃圾收集的效率。使用標記-清除演算法的垃圾收集器在工作時要消耗相當多的 CPU 資源。早期的 Lisp 運行環境收集內存垃圾的時間竟佔到了系統總運行時間的 40% !——垃圾收集效率的低下直接造就了 Lisp 語言在執行速度方面的壞名聲;直到今天,許多人還條件反射似地誤以為所有 Lisp 程序都奇慢無比。
第二,減少垃圾收集時的內存佔用。這一問題主要出現在復制演算法中。盡管復制演算法在效率上獲得了質的突破,但犧牲一半內存空間的代價仍然是巨大的。在計算機發展的早期,在內存價格以 KB 計算的日子裡,浪費客戶的一半內存空間簡直就是在變相敲詐或攔路打劫。
第三,尋找實時的垃圾收集演算法。無論執行效率如何,三種傳統的垃圾收集演算法在執行垃圾收集任務時都必須打斷程序的當前工作。這種因垃圾收集而造成的延時是許多程序,特別是執行關鍵任務的程序沒有辦法容忍的。如何對傳統演算法進行改進,以便實現一種在後台悄悄執行,不影響——或至少看上去不影響——當前進程的實時垃圾收集器,這顯然是一件更具挑戰性的工作。
研究者們探尋未知領域的決心和研究工作的進展速度同樣令人驚奇:在 1970 年代到 1980 年代的短短十幾年中,一大批在實用系統中表現優異的新演算法和新思路脫穎而出。正是因為有了這些日趨成熟的垃圾收集演算法,今天的我們才能在 Java 或 .NET 提供的運行環境中隨心所欲地分配內存塊,而不必擔心空間釋放時的風險。
標記-整理( Mark-Compact )演算法
標記-整理演算法是標記-清除演算法和復制演算法的有機結合。把標記-清除演算法在內存佔用上的優點和復制演算法在執行效率上的特長綜合起來,這是所有人都希望看到的結果。不過,兩種垃圾收集演算法的整合並不像 1 加 1 等於 2 那樣簡單,我們必須引入一些全新的思路。 1970 年前後, G. L. Steele , C. J. Cheney 和 D. S. Wise 等研究者陸續找到了正確的方向,標記-整理演算法的輪廓也逐漸清晰了起來:
在我們熟悉的餐廳里,這一次,垃圾收集機器人不再把餐廳分成兩個南北區域了。需要執行垃圾收集任務時,機器人先執行標記-清除演算法的第一個步驟,為所有使用中的餐巾紙畫好標記,然後,機器人命令所有就餐者帶上有標記的餐巾紙向餐廳的南面集中,同時把沒有標記的廢舊餐巾紙扔向餐廳北面。這樣一來,機器人只消站在餐廳北面,懷抱垃圾箱,迎接撲面而來的廢舊餐巾紙就行了。
實驗表明,標記-整理演算法的總體執行效率高於標記-清除演算法,又不像復制演算法那樣需要犧牲一半的存儲空間,這顯然是一種非常理想的結果。在許多現代的垃圾收集器中,人們都使用了標記-整理演算法或其改進版本。
增量收集( Incremental Collecting )演算法
對實時垃圾收集演算法的研究直接導致了增量收集演算法的誕生。
最初,人們關於實時垃圾收集的想法是這樣的:為了進行實時的垃圾收集,可以設計一個多進程的運行環境,比如用一個進程執行垃圾收集工作,另一個進程執行程序代碼。這樣一來,垃圾收集工作看上去就彷彿是在後台悄悄完成的,不會打斷程序代碼的運行。
在收集餐巾紙的例子中,這一思路可以被理解為:垃圾收集機器人在人們用餐的同時尋找廢棄的餐巾紙並將它們扔到垃圾箱里。這個看似簡單的思路會在設計和實現時碰上進程間沖突的難題。比如說,如果垃圾收集進程包括標記和清除兩個工作階段,那麼,垃圾收集器在第一階段中辛辛苦苦標記出的結果很可能被另一個進程中的內存操作代碼修改得面目全非,以至於第二階段的工作沒有辦法開展。
M. L. Minsky 和 D. E. Knuth 對實時垃圾收集過程中的技術難點進行了早期的研究, G. L. Steele 於 1975 年發表了題為“多進程整理的垃圾收集( Multiprocessing compactifying garbage collection )”的論文,描述了一種被後人稱為“ Minsky-Knuth-Steele 演算法”的實時垃圾收集演算法。 E. W. Dijkstra , L. Lamport , R. R. Fenichel 和 J. C. Yochelson 等人也相繼在此領域做出了各自的貢獻。 1978 年, H. G. Baker 發表了“串列計算機上的實時表處理技術( List Processing in Real Time on a Serial Computer )”一文,系統闡述了多進程環境下用於垃圾收集的增量收集演算法。
增量收集演算法的基礎仍是傳統的標記-清除和復制演算法。增量收集演算法通過對進程間沖突的妥善處理,允許垃圾收集進程以分階段的方式完成標記、清理或復制工作。詳細分析各種增量收集演算法的內部機理是一件相當繁瑣的事情,在這里,讀者們需要了解的僅僅是: H. G. Baker 等人的努力已經將實時垃圾收集的夢想變成了現實,我們再也不用為垃圾收集打斷程序的運行而煩惱了。
分代收集( Generational Collecting )演算法
和大多數軟體開發技術一樣,統計學原理總能在技術發展的過程中起到強力催化劑的作用。 1980 年前後,善於在研究中使用統計分析知識的技術人員發現,大多數內存塊的生存周期都比較短,垃圾收集器應當把更多的精力放在檢查和清理新分配的內存塊上。這個發現對於垃圾收集技術的價值可以用餐巾紙的例子概括如下:
如果垃圾收集機器人足夠聰明,事先摸清了餐廳里每個人在用餐時使用餐巾紙的習慣——比如有些人喜歡在用餐前後各用掉一張餐巾紙,有的人喜歡自始至終攥著一張餐巾紙不放,有的人則每打一個噴嚏就用去一張餐巾紙——機器人就可以制定出更完善的餐巾紙回收計劃,並總是在人們剛扔掉餐巾紙沒多久就把垃圾撿走。這種基於統計學原理的做法當然可以讓餐廳的整潔度成倍提高。
D. E. Knuth , T. Knight , G. Sussman 和 R. Stallman 等人對內存垃圾的分類處理做了最早的研究。 1983 年, H. Lieberman 和 C. Hewitt 發表了題為“基於對象壽命的一種實時垃圾收集器( A real-time garbage collector based on the lifetimes of objects )”的論文。這篇著名的論文標志著分代收集演算法的正式誕生。此後,在 H. G. Baker , R. L. Hudson , J. E. B. Moss 等人的共同努力下,分代收集演算法逐漸成為了垃圾收集領域里的主流技術。
分代收集演算法通常將堆中的內存塊按壽命分為兩類,年老的和年輕的。垃圾收集器使用不同的收集演算法或收集策略,分別處理這兩類內存塊,並特別地把主要工作時間花在處理年輕的內存塊上。分代收集演算法使垃圾收集器在有限的資源條件下,可以更為有效地工作——這種效率上的提高在今天的 Java 虛擬機中得到了最好的證明。
應用浪潮
Lisp 是垃圾收集技術的第一個受益者,但顯然不是最後一個。在 Lisp 語言之後,許許多多傳統的、現代的、後現代的語言已經把垃圾收集技術拉入了自己的懷抱。隨便舉幾個例子吧:誕生於 1964 年的 Simula 語言, 1969 年的 Smalltalk 語言, 1970 年的 Prolog 語言, 1973 年的 ML 語言, 1975 年的 Scheme 語言, 1983 年的 Mola-3 語言, 1986 年的 Eiffel 語言, 1987 年的 Haskell 語言……它們都先後使用了自動垃圾收集技術。當然,每一種語言使用的垃圾收集演算法可能不盡相同,大多數語言和運行環境甚至同時使用了多種垃圾收集演算法。但無論怎樣,這些實例都說明,垃圾收集技術從誕生的那一天起就不是一種曲高和寡的“學院派”技術。
對於我們熟悉的 C 和 C++ 語言,垃圾收集技術一樣可以發揮巨大的功效。正如我們在學校中就已經知道的那樣, C 和 C++ 語言本身並沒有提供垃圾收集機制,但這並不妨礙我們在程序中使用具有垃圾收集功能的函數庫或類庫。例如,早在 1988 年, H. J. Boehm 和 A. J. Demers 就成功地實現了一種使用保守垃圾收集演算法( Conservative GC Algorithmic )的函數庫。我們可以在 C 語言或 C++ 語言中使用該函數庫完成自動垃圾收集功能,必要時,甚至還可以讓傳統的 C/C++ 代碼與使用自動垃圾收集功能的 C/C++ 代碼在一個程序里協同工作。
1995 年誕生的 Java 語言在一夜之間將垃圾收集技術變成了軟體開發領域里最為流行的技術之一。從某種角度說,我們很難分清究竟是 Java 從垃圾收集中受益,還是垃圾收集技術本身借 Java 的普及而揚名。值得注意的是,不同版本的 Java 虛擬機使用的垃圾收集機制並不完全相同, Java 虛擬機其實也經過了一個從簡單到復雜的發展過程。在 Java 虛擬機的 1.4.1 版中,人們可以體驗到的垃圾收集演算法就包括分代收集、復制收集、增量收集、標記-整理、並行復制( Parallel Copying )、並行清除( Parallel Scavenging )、並發( Concurrent )收集等許多種, Java 程序運行速度的不斷提升在很大程度上應該歸功於垃圾收集技術的發展與完善。
盡管歷史上已經有許多包含垃圾收集技術的應用平台和 操作系統 出現,但 Microsoft .NET 卻是第一種真正實用化的、包含了垃圾收集機制的通用語言運行環境。事實上, .NET 平台上的所有語言,包括 C# 、 Visual Basic .NET 、 Visual C++ .NET 、 J# 等等,都可以通過幾乎完全相同的方式使用 .NET 平台提供的垃圾收集機制。我們似乎可以斷言, .NET 是垃圾收集技術在應用領域里的一次重大變革,它使垃圾收集技術從一種單純的技術變成了應用環境乃至操作系統中的一種內在 文化 。這種變革對未來軟體開發技術的影響力也許要遠遠超過 .NET 平台本身的商業價值。
大勢所趨
今天,致力於垃圾收集技術研究的人們仍在不懈努力,他們的研究方向包括分布式系統的垃圾收集、復雜事務環境下的垃圾收集、資料庫等特定系統的垃圾收集等等。
但在程序員中間,仍有不少人對垃圾收集技術不屑一顧,他們寧願相信自己逐行編寫的 free 或 delete 命令,也不願把垃圾收集的重任交給那些在他們看來既蠢又笨的垃圾收集器。
我個人認為,垃圾收集技術的普及是大勢所趨,這就像生活會越來越好一樣毋庸置疑。今天的程序員也許會因為垃圾收集器要佔用一定的 CPU 資源而對其望而卻步,但二十多年前的程序員還曾因為高級語言速度太慢而堅持用機器語言寫程序呢!在硬體速度日新月異的今天,我們是要吝惜那一點兒時間損耗而踟躇不前,還是該堅定不移地站在代碼和運行環境的凈化劑——垃圾收集的一邊呢?