国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

[CLR via C#]21. 自動內存管理(垃圾回收機制)

2019-11-17 03:06:41
字體:
來源:轉載
供稿:網友

[CLR via C#]21. 自動內存管理(垃圾回收機制)

目錄

  • 理解垃圾回收平臺的基本工作原理

  • 垃圾回收算法
  • 垃圾回收與調試

  • 使用終結操作來釋放本地資源
  • 對托管資源使用終結操作

  • 是什么導致Finalize方法被調用

  • 終結操作揭秘

  • Dispose模式:強制對象清理資源

  • 使用實現了Dispose模式的類型

  • C#的using語句

  • 手動監視和控制對象的生存期

  • 對象復活

  • 線程劫持

  • 大對象

一、理解垃圾回收平臺的基本工作原理

  1. 值類型(含所有枚舉類型)、集合類型、String、Attribute、Delegate和Event所代表的資源無需執行特殊的清理操作。
  2. 如果一個類型代表著或包裝著一個非托管資源或者本地資源(比如數據庫連接、套接字、mutex、位圖等),那么在對象的內存準備回收時,必須執行資源清理代碼。
  3. CLR要求所有的資源都從托管堆分配。
  4. 進程初始化時,CLR要保留一塊連續的地址空間,這個地址空間最初沒有對應的物理存儲空間。這個地址空間就是托管堆。托管堆還維護著一個指針,可以稱為NextObjPtr。它指向下一個對象在堆中的分配位置。剛開始時,NextObjPtr設為保留地址空間的基地址。IL指令使用newobj創建一個對象。newobj指令將導致CLR執行以下步驟:
    1. 計算類型(及其所有基類型)所需要的字節數。
    2. 加上對象的額外開銷的字節數——“類型對象指針”和“同步塊索引”。

    3. CLR檢查保留區域是否能分配出相應的字節數。如果托管堆有足夠的可用空間,對象將被放入。注意對象這在NextObjPtr指針指向的地址放入的,并且為它分配的字節會被清零。接著,調用類型的實例構造函數(為this參數傳遞NextObjPtr),IL指令newobj將返回對象的地址。就在地址返回之前,NextObjPtr指針的值會加上對象占據的字節數,這樣就會得到一個新的NextObjPtr值,它指向下一個對象放入托管堆時的地址。
  5. 托管堆之所以能這么做,是因為它做了一個相當大膽的假設——地址空間和存儲是無限的。這個假設顯然是荒謬的。所以,托管堆必須通過某種機制來允許它做這樣的假設。這種機制就是垃圾回收。
  6. 對象不斷的被創建,NextObjPtr也在不斷的增加,如果NextObjPtr超過了地址空間的末尾,表明托管堆已滿,就必須強制執行一次垃圾回收。

二、 垃圾回收算法

  1. 每個應用程序都包含一組。每個根都是一個存儲位置,其中包含指向引用類型對象的指針。該指針要么引用托管堆中的一個對象,要么為null。只有引用類型的變量才會被認為是根;值類型的變量永遠不被認為是根。
  2. 垃圾回收開始執行時,它假設堆中所有對象都是垃圾。
    1. 第一個階段為標記階段。這個階段,垃圾回收器沿著線程棧向上檢查所有根。如果發現一個根引用了一個對象,就進行”標記”。該標記具有傳遞性。標記好根和它的字段引用的對象之后,垃圾回收器會檢查下一個根,并繼續標記對象。如果垃圾回收期試圖標記先前已經標記了的根,就會停止沿著這個路徑走下去。檢查好所有根之后,堆中將包含一組已標記和未標記的對象。已標記的對象是通過應用程序的代碼可以到達的對象,而未標記的對象是不可達的。不可達的對象就是垃圾,它們的內存是可以回收的。
    2. 第二個階段為壓縮(可以理解成"內存碎片整理")階段。在這個階段中,垃圾回收器線性遍歷堆,以尋找未標記對象的連續內存塊。如果這個內存塊較小,垃圾回收器會忽略它們。反之,垃圾回收器會把非垃圾的對象移動到這里已壓縮堆,其實在這是內存碎片整理或許更會適用。自然的,包含那些”指向這些對象的指針”的變量和CPU寄存器現在都會變得無效。所以,垃圾回收器必須重新訪問應用程序的所有根,并修改它們來指向對象的新內存位置。堆內存壓縮之后,托管堆的NextObjPtr指針將指向緊接在最后一個非垃圾回收對象之后的位置。
  3. 所以,垃圾回收器會造成顯著的損失,這是使用托管堆的主要缺點。當然,垃圾回收只在第0代滿的時候才會發生。在此之前,托管堆性能遠遠高于C運行時堆。

三、垃圾回收與調試

  1. 當JIT編譯器將方法的IL代碼編譯成本地代碼時,JIT編譯器會檢查兩點:定義方法的程序集在編譯時沒有優化;進行當前在一個調試器中執行。如果這兩點都成立,JIT編譯器在生成方法的內部根表時,會將變量的生存期手動延長至方法結束。

四、使用終結操作來釋放本地資源

  1. 終結是CLR提供的一種機制,允許對象在垃圾回收器回收其內存之前執行一些得體的清理工作。
  2. 任何包裝了本地資源的類型都必須支持終結操作。簡單的說,類型實現了一個命名為Finalize的方法。當垃圾回收期判斷一個對象是垃圾時,會調用對象的Finalize方法。
  3. C#團隊認為,Finalize方法是編程語言中需要特殊語法的一種方法。在C#中,必須在類名前加一個~符號來定義Finalize方法。
Internal sealed class SomeType {     ~SomeType(){         //這里的代碼會進入Finalize方法    }}

  5. 編譯上述代碼,會發現C#編譯器實際是在模塊的元數據中生成一個名為Finalize的PRotected override方法。方法主體被放到try塊中,finally塊放入了一個對base.Finalize的調用。

  6.實現Finalize方法時,一般都會調用Win32 CloseHandle函數,并向該函數傳遞本地資源的句柄。

五、對托管資源使用終結操作

  1. 永遠不要對托管資源使用終結操作,這是有一種非常好的編程習慣。因為對托管資源使用終結操作是一種非常高級的編碼方式,只有極少數情況下才會用到。
  2. 設計一個類型時,處于以下幾個性能原因,應避免使用Finalize方法:
    1. 可終結的對象要花費更長的時間來分配,因為指向它們的指針必須先放到終結列表中。("終結列表"在第七節會說到)
    2. 可終結對象會被提升到較老的一代,這會增加內存壓力,并在垃圾回收器判定為垃圾時,阻止回收。除此之外,對該對象直接或間接引用的對象都會提升到較老的一代。("代"在第十三節會說到)
    3. 可終結的對象會導致應用程序運行緩慢,因為每個對象在進行回收時,需要對它們進行額外操作。
  3. 我們無法控制Finalize方法何時運行。CLR不保證各個Finalize的調用順序。

六、是什么導致Finalize方法被調用

  1. 第0代滿 只有第0代滿時,垃圾回收器會自動開始。該事件是目前導致調用Finalize方法最常見的一種方式。("代"在第十三節會說到)
  2. 代碼顯式調用System.GC的靜態方法Collect 代碼可以顯式請求CLR執行即時垃圾回收操作。
  3. Windows內存不足 當Windows報告內存不足時,CLR會強制執行垃圾回收。
  4. CLR卸載AppDomain 一個ApppDomain被卸載時,CLR認為該AppDomain不存在任何根,因此會對所有代的對象執行垃圾回收。
  5. CLR關閉 一個進程結束時,CLR就會關閉。CLR關閉會認為進程中不存在 任何根,因此會調用托管堆中所有的Finalize方法,最后由Windows回收內存。

七、終結操作揭秘

  1. 應用程序創建一個新對象時,new操作符會從堆中分配內存。如果對象的類型定義了Finalize方法,那么在該類型的實例構造器調用之前,會將一個指向該對象的指針放到一個終結列表(finalization list)中。
  2. 終結列表是由垃圾回收器控制的一個內部數據結構。列表中的每一項都指向一個對象,在回收該對象之前,會先調用對象的Finalize方法。
  3. 下圖1展示了包含幾個對象的一個托管堆。有的對象從應用程序的根可達,有的不可達(垃圾)。對象C,E,F,I,J被創建時,系統檢測到這些對象的類型定義來了Finalize方法,所有指向這些對象的指針要添加到終結列表中。

  4. 垃圾回收開始時,對象B,E,G,H,I和J被判定為垃圾。垃圾回收器掃描終結列表以查找指向這些對象的指針。找到一個指針后,該指針會從終結列表中移除,并追加到freachable隊列中。freachable隊列(發音是“F-reachable”)是垃圾回收器的內部數據結構。Freachable隊列中的每個指針都代表其Finalize方法已準備好調用的一個對象。圖2展示了回收完畢后托管堆的情況。

  5. 從圖2中我們可以看出B,E和H已經從托管堆中回收了,因為它們沒有Finalize方法,而E,I,J則暫時沒有被回收,因為它們的Finalize方法還未調用。
  6. 一個特殊的高優先級的CLR線程負責調用Finalize方法。使用專用的線程可避免潛在的線程同步問題。freachable隊列為空時,該線程將睡眠。當隊列中有記錄項時,該線程就會被喚醒,將每一項從freachable隊列中移除,并調用每一項的 Finalize方法。
  7. 如果一個對象在freachable隊列中,那么意味這該對象是可達的,不是垃圾。
  8. 原本,當對象不可達時,垃圾回收器將把該對象當成垃圾回收了,可是當對象進入freachable隊列時,有奇跡般的”復活”了。然后,垃圾回收器壓縮(內存脆片整理)可回收的內存,特殊的CLR線程將清空freachable隊列,并調用其中每個對象的Finalize方法。
  9. 垃圾回收器下一次回收時,發現已終結的對象成為真正的垃圾,因為應用程序的根不再指向它,freachhable隊列也不再指向它。所以,這些對象的內存會直接回收。
  10. 整個過程中,可終結對象需要執行兩次垃圾回收器才能釋放它們占用的內存。可在實際開發中,由于對象可能被提升到較老的一代,所以可能要求不止兩次進行垃圾回收。圖3展示了第二次垃圾回收后托管堆中的情況。

八、Dispose模式:強制對象清理資源

  1. Finalize方法非常有用,因為它確保了當托管對象的內存被釋放時,本地資源不會泄漏。但是,Finalize方法的問題在于,他的調用時間不能保證。另外,由于他不是公共方法,所以類的用戶不能顯式調用它。
  2. 類型為了提供顯式進行資源清理的能力,提供了Dispose模式。
  3. 所有定義了Finalize方法的類型都應該同時實現Dispose模式,使類型的用戶對資源的生存期有更多的控制。

九、使用實現了Dispose模式的類型

  1. 調用Dispose或Close只是為了能在一個確定的時間強迫對象執行清理;這兩個方法并不能控制托管堆中的對象所占用的內存的生存期。這意味著即使一個對象已完成了清理,仍然可在它上面調用方法,但會拋出ObjectDisposedException異常。
  2. 建議只有在以下兩種情況下才調用Dispose或Close:
    1. a) 確定必須清理資源
    2. b) 確定可以安全的調用Dispose或Close,并希望將對象從終結列表中刪除,禁止對象提升到下一代,從而提升性能。

十、C#的using語句

  1. 如果決定顯式地調用Dispose和Close這兩個方法之一,強烈建議把它們放到一個異常處理finally中。這樣可以保證清理代碼得到執行。
  2. Using語句就是一種對第1點進行簡化的語法。

十一、手動監視和控制對象的生存期

  1. CLR為每一個AppDomain都提供了一個GC句柄表。該表允許應用程序監視對象的生存期,或手動控制對象的生存期。
  2. 在一個AppDomain創建之初,該句柄表是空的。句柄表中的每個記錄項都包含以下兩種信息:一個指針,它指向托管堆上的一個對象;一個標志(flag),它指出你想如何監視或控制對象。
  3. 為了在這個表中添加或刪除記錄項,應用程序要使用如下所示的System.Runtime.InteropServices.GCHandle類型。

十二、對象復活

  1. 前面說過,需要終結的一個對象被認為死亡時,垃圾回收器會強制是該對象重生,使它的Finalize方法得以調用。Finalize方法調用之后,對象才真正的死亡。
  2. 需要終結的一個對象會經歷死亡、重生、在死亡的”三部曲”。一個死亡的對象重生的過程稱為重生
  3. 復活一般不是一件好事,應避免寫代碼來利用CLR這個”功能”。

十三、代

  1. 代是CLR垃圾回收器采用的一種機制,它唯一的目的就是提升應用程序的性能。
  2. 一個基于代的垃圾回收器做出了以下幾點假設:
    1. 對象越新,生存期越短。
    2. 對象越老,生存期越長。
    3. 回收堆的一部分,速度快于回收整個堆。
  3. 代的工作原理:
    1. 托管堆在初始化時不包含任何對象。添加到堆的對象稱為第0代對象。第0代對象就是那些新構造的對象,垃圾回收器從未檢查過它們。圖4展示了一個新啟動的應用程序,它分配了5個對象。過會兒,對象C和E將變得不可達。
    2. CLR初始化時,它會為第0代對象選擇一個預算容量,假定為256K(實際容量可能有所不同)。所以,如果分配一個新對象造成第0代超過預算,就必須啟動一次垃圾回收。假定對象A到E剛好占用256K內存。對象F分配時,垃圾回收器必須啟動。垃圾回收器判定對象C和E為垃圾,因為會壓縮(內存碎片整理)對象D,使其與對象B相鄰。之所以第0代的預算容量為256K,是因為所有這些對象都能裝入CPU的L2緩存,使之壓縮(內存碎片整理)能以非常快的速度完成。在垃圾回收中存活的對象(A、B和D)被認為是第1代對象。第1代對象已經經歷垃圾回收的一次檢查。此時的對如圖5所示。
    3. 一次垃圾回收后,第0代就不包含任何對象了。和前面一樣,新對象會分配到第0代中。在圖6中,應用程序繼續運行,并新分配了對象F到對象K。另外,隨著應用程序繼續運行,對象B、H和J變得不可達,它們的內存將在某一個回收。
    4. 現在,假定分配新對象L會造成第0代超過256KB的預算。由于第0代達到預算,所以必須啟動垃圾回收器。開始一次垃圾回收時,垃圾回收器必須決定檢查哪些代。
    5. 前面說過,當CLR初始化時,他為第0代對象選擇了一個預算。同樣的,它還必須為第1代選擇一個預算。假定為第1代選擇的預算為2MB。
    6. 垃圾回收開始時,垃圾回收器還會檢查第1代占據了多少內存。由于在本例中。第一代占據的內存遠遠小于2MB,所以垃圾回收器只檢查第0代。因為此時垃圾回收器只檢查第0代,忽略第1代,所以大大加快了垃圾回收器的速度。但是,對性能最大的提升就是現在不必遍歷整個托管堆。如果一個對象引用了一個老對象,垃圾回收器就可以忽略那個老對象的所有內部引用,從而能更快的構造好可達對象的圖。
    7. 如圖7所示,所有幸存下來的第0代對象變成了第1代的一部分。由于垃圾回收器沒有檢查第1代,所以對象B的內存并沒有被回收,即使它在上次垃圾回收時變得不可達。在一次垃圾回收后,第0代不包含任何對象,等著分配新對象。
    8. 假定程序繼續運行,并分配對象L到對象O。另外,在運行過程中,應用程序停止使用對象G,I,M,是它們變得不可達。此時的托管堆如圖8所示。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 多伦县| 台州市| 陈巴尔虎旗| 临邑县| 广德县| 浦城县| 长寿区| 凤翔县| 那坡县| 英超| 铜鼓县| 外汇| 来安县| 古浪县| 甘肃省| 吴川市| 荥阳市| 南乐县| 莆田市| 北辰区| 玛纳斯县| 成都市| 吴堡县| 雷波县| 鄢陵县| 班戈县| 错那县| 化德县| 锦屏县| 普安县| 辽中县| 北流市| 黑龙江省| 布尔津县| 吉安县| 青田县| 蒙山县| 瑞安市| 广丰县| 隆昌县| 清原|