目錄
- 前言
- 分配記憶體和資源初始化
- 清理本地資源
- 回收記憶體 & 垃圾回收演算法
- 垃圾回收機制:代
前言:資源的生存週期
1、new一個物件時,呼叫IL命令newobj,為資源型別分配記憶體。
2、初始化記憶體,建構函式初始化資源的狀態。
3、程式中來回的呼叫、訪問資源。
4、摧毀資源的狀態並進行清理。
5、釋放記憶體。垃圾回收執行這一步。
一、分配記憶體和資源初始化
第1與第2步—如何分配記憶體和資源初始化?
首先CLR規定所有的資源都從託管堆中分配。此託管堆維護物件資源,會為我們自動管理物件狀態。
程式初始化時,CLR會預留一塊連續的地址空間即託管堆,但是沒有對應的物理儲存空間。此託管堆上維護者一個指標,它指向下個物件在託管堆中的分配位置。
new操作符會生成一個IL指令newobj,指令會指導CLR進行以下工作。
A、計算型別以及基類的欄位所需要的空間。
B、加上物件的開銷所需的位元組數(型別物件指標以及同步塊索引)。
C、CLR檢查保留區域是否能夠提供分配物件所需的位元組數,如果有就提交儲存。物件會在指標NewObjPtr指向的位置放入,為物件分配的位元組數清零,並呼叫例項化構造器返回物件的地址。
當前指標會加上物件佔據的位元組數,成為一個新址,下一個物件的儲存地址。
託管堆上分配物件的前提是空間記憶體足夠,那就引出了下一個機制—垃圾回收機制,來回收記憶體,釋放資源。
二、清理本地資源
第4步—怎樣摧毀本地資源的狀態並進行清理?
方式一:隱式終結—Finalize()
定義:垃圾回收器會在回收記憶體之前執行Finzlize()(如果此類實現了Finalize方法),垃圾回收會自動呼叫此方法。垃圾回收器會因隱式執行。
語法:是在類名前加~,例如:~方法名(){}—Finalize方法。
觸發:第0代滿、顯示呼叫System.GC的Collect方法、記憶體不足、解除安裝AppDomain
原理:一個實現了Finalize方法的物件new之後,會在垃圾回收器維護的一個終結列表中新增一個指標指向這個新分配的這個物件。在回收記憶體之前呼叫它的Finalize方法。
不足:Finalize釋放的是託管資源,垃圾回收器會在進行垃圾回收的時候釋放託管資源,我們不確定下次的垃圾回收發生在何時,我們想自己控制垃圾回收並控制菲菲託管資源的釋放。
所以下面就使用顯示摧毀資源狀態(前提是確定不再使用,確認需要關閉)。比如:資料庫連線、檔案讀寫
方式二:顯示終結—Dispose()、Close()
我們通過書上的例子更直接:
下面具體討論一下我們經常使用的Close()以及Dispose()
首先我們看到了熟悉的 Finalize()方法—>~SafeHandle() ,還有需要我們討論的Dispose()方法和Close()方法。另外,還有一個帶有引數的Dispose(Boolean disposeing)虛方法。
這裡的資源釋放統一處理 Dispose(Boolean disposeing) 方法,如果引數為true,會標記此物件資源顯示關閉,但是沒有終結,可以正常訪問欄位。如果是false,那就終結物件,回收記憶體。
因為繼承了IDispose介面,所以要實現一個無參的Dispose()方法,而我們經常使用的Close()是因為出於習慣覺得有一個叫Close()的方法似乎更親切。所以就新增了一個Close()方法。沒有其他特殊用途。
當我們呼叫Dispose()或者Close()方法時,物件本身的記憶體還沒有釋放,仍然需要垃圾回收器來回收記憶體。
所以到目前為止我們可以通過三種方式來摧毀資源:
1、顯示呼叫Dispose()
2、顯式呼叫Close()
3、等待垃圾回收時,垃圾回收器自動呼叫Finalize方法進行摧毀。
4、一種Dispose()和Close()方法的變相模式using(){}
三、垃圾回收
第5步—如何釋放記憶體?
垃圾回收器檢查託管堆中是否有應用程式不再使用的物件。有,回收記憶體(如果回收後,記憶體仍然不夠,就丟擲記憶體溢位異常)。
垃圾回收器怎樣判斷物件正在使用?
每個應用程式都包含一組根,每個根都是一個儲存盒子,裡面包含著引用物件指標(要麼引用一個物件,要麼為null)。
例如類中定義的任何靜態欄位會被認為有一個根,方法中的任何引數和區域性變數也會被認為有一個根。只有引用型別的變數才被認為是根。
當然這裡有個前提:只有引用型別才能認為是根,值型別除外。
借用一下書上的例子:類
JIT在生成CPU程式碼的同時還會生成方法在本地CPU指令中的一個位元組偏移範圍的記錄項,這個記錄項也包含著根的一組記憶體地址和CPU暫存器。
上面的類在第一次呼叫方法 WriteBytes 的時候,JIT會將IL程式碼翻譯成CPU指令,如下(x86 CPU):
1、暫存器:
ebx在偏移到00000003處開始為暫存器的根,到迴圈結束 00000028處結束根。此類為例項,所以會有一個this指標,通過','後面的ecx暫存器傳遞,並儲存到前面的暫存器ebx。
同樣,esi在偏移到00000005處開始為暫存器的根,直到00000028根結束。它通過暫存器edx傳遞bytes[],並將陣列存入到暫存器esi。
對於edi來說,它傳遞的是Int32型別,值型別不會有根。
後面的ecx從0000000f開始作為根,到000001e處根結束。
2、垃圾回收:如果在0000017處發生垃圾回收
首先確定,00000017處發生的垃圾回收,沒有到達ebx(this指標)、esi(byte[])的根結束位置00000028 ,也沒有到達 ecx(m_textWriter)根結束位置0000001e。
A、收集根:
(1)這三個暫存器中引用指向的物件都是根,而且這些根中所引用的堆中的物件也不能回收。
(2)其次垃圾回收器會檢查執行緒棧上行,檢查每個方法的內部表來確定所有呼叫方法的根。
(3)最後垃圾回收器將遍歷所有型別物件,來獲取靜態欄位中儲存的根集合。
B、標記階段
(1)垃圾回收器開始執行時,它會假設堆中的所有物件都是垃圾。它會假設執行緒棧和堆沒有引用關聯,沒有CPU暫存器引用堆中的物件。也沒有靜態欄位引用堆中的物件。
(2)接著進入標記階段,沿著執行緒棧上行檢查所有根,如果發現一個根引用了一個物件,就對這個物件進行標記(同步塊索引欄位上開啟一個bit=1的標識)。
收集根並標記完後,會有標記和未標記的物件。標記的就是程式可以繼續訪問的,反之就是不可達的垃圾。就會對垃圾進行回收記憶體。
C、壓縮階段
(1)垃圾回收器會線性遍歷堆,遇到垃圾物件時,檢查一下連續記憶體塊,如果較小就忽略,較大就會將非垃圾物件移動到這裡。
(2)但是非垃圾物件之前的地址和暫存器等都會失效,垃圾回收器也會重新訪問根,生成新的地址等等。
這樣程式記憶體的碎片化就得到大幅度的控制,當然這也是犧牲了些許的效能。
四、代
代:是垃圾回收器採用的一種機制,目的就是為了提高程式效能。
根據代機制,我們可以做出以下假設:
- 物件越新,回收可能性非常大。
- 物件越老,回收可能性非常小。
- 回收堆中的部分,速度快於回收整個堆。
我們的託管堆初始化時不會包含任何物件,我們初始化的物件會新增到託管堆上,這些物件我們稱為第0代。第0代的儲存上限為256KB,第一代為2M·····
此時我們標記第0代物件在堆上的儲存上限為256KB(只是假設一下),如下圖:
5個物件A、B、C、D、E。程式執行一會之後,C和E變得不可達,等待垃圾回收器來回收記憶體。
每當第0代滿時,也就是當前堆中的物件達到了上限256KB,這時垃圾回收器開始執行垃圾回收,
如果此時分配新的物件F時,出現A~E達到分配上限256KB,垃圾回收器就會壓縮D使得和之前可用記憶體連續起來。
C和E回收了記憶體。這樣A、B、D進入第一代,第0代空,如下圖:
執行一段時間,B、H、J也是不可達狀態,同時在新分配L時第0代達到了上限256KB,如下圖:
現在第0代滿,垃圾回收器執行,它會壓縮I、K的記憶體與G連續。同時回收H、J。
此時第一代這個雖然有不可達的B物件,但是第一代沒有達到上限2M,垃圾回收器就不會對B進行回收。
回收後:我們看到沒有對B進行回收,那是因為第一代沒有達到上限2M,這一切為了效能,因為第一代中出現垃圾的頻率一般遠遠低於第0代甚至沒有垃圾。
這樣垃圾回收器就寧願讓垃圾暫時呆在那裡,暫時不去回收。這樣節省了時間,增加了效率。
程式依然執行,就會有更多的物件分配到堆上,同時產生更多的的垃圾,如下圖:
此時當分配P時,第0代滿,執行垃圾回收,回收地0代P、R
此時垃圾回收器檢測到第一代也超過限額2M,就檢測第一代中的物件進行垃圾回收。知道這個時候第一代的垃圾才有幸回收。
回收後:此時會將第一代剩下的升級到第二代中,原來在第0代存活的物件,也會升級到第一代中。
垃圾回收器只有三代。因為CLR會根據實際情況進行自動調節。3代足夠。
PS:在CLR初始化時,會對第0代、第一代、第二代進行記憶體限額設定:256KB、2M、10M。限額越大執行垃圾回收的頻率越低。
只在第0代滿時進行垃圾回收,當第0代滿,此時第一代也滿,才會對第一代進行垃圾回收。所以效率上是可以保證的。
當CLR檢測到回收第0代物件後,幾乎沒有回收多少記憶體,此時就會調整上限到512KB。同樣如果回收的垃圾很多,那調整到128KB。以此類推,自動調劑。
這樣的調節也會應用於第一代、第二代。