目錄
- 前言
- 分配內存和資源初始化
- 清理本地資源
- 回收內存 & 垃圾回收算法
- 垃圾回收機制:代
前言:資源的生存周期
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。以此類推,自動調劑。
這樣的調節也會應用於第一代、第二代。