關於C#的垃圾回收機制,Finalize和Dispose的區別(自認爲很清晰了,有疑問的評論)

來到個新地方,新學習C#,前面看到C#的垃圾回收,Finalize和Dispose時,老是隻知其一;不知其二,迷迷糊糊。此次好了,前面連續兩次面試問到這個問題,腦子裏不是很清晰,加上用英文來表達,更是雪上加霜的感受。html

 

回來,好好看了相關資料,從網上看,總沒有人能說的很清晰,每每很深奧的樣子,拿了本<C# language 7.0>,這樣中英文結合看,總算清晰了。程序員

如今主要是找工做,沒有時間寫詳細,先就說個重點:面試

1. 垃圾回收器GC負責託管對象的回收。GC經過從應用根對象開始(如全局變量,靜態變量等)去訪問對象到來判斷對象是否能回收,若是沒法到達對象,則對像能夠被回收。算法

而不是經過引用計數法來判斷對象是否須要被回收,由於引用計數沒法解決一個循環引用的兩個對象的回收,他們的引用計數都爲1,但實際這兩個對象是整個應用程序中的孤島對象,數據庫

從應用程序根對象開始追蹤是沒法訪問到的,應該被回收。(後續詳細解釋)框架

2. 垃圾回收很耗性能,通常只在分配新對象發現空間不夠用時才觸發回收。也就是說,什麼時候回收對用戶來講是不可控,未知的。socket

具體回收時採用的算法是分步回收,即分代回收的方法。其基於統計原理,新產生的對象老是有最大的可能性不被使用而須要回收,好比新調用函數中的新分配的局部對象;而通過幾回回收週期中存活下來的對象,則更不可能被回收掉,如全局變量等。函數

  GC初始化默認每一個對象都是須要回收的,回收觸發時,經過檢測從根對象開始是否能訪問到來判斷對象是否真的能夠回收。GC將對象標籤爲3代,分別是0,1,和2代對象。0代是尚未通過一次回收檢測的對象(沒檢測過,默認都是能夠回收的),1表明示通過一次回收檢測後存活下來(即不能回收)的對象,而2表明示通過2次以上檢測仍存活的對象。性能

 GC檢測依照順序,分別檢測0代,1代和2代。可是若是檢測到0代,回收部分對象後,發現空間已經足夠新對象使用,回收檢測馬上中止。若是仍然不夠,纔會繼續檢測1代對象,直至2代對象。經過這樣的方法,避免垃圾回收機制產生做用時的性能消耗。大部分狀況下,0代檢測就能釋放足夠空間應付新對象的分配,這樣後續的垃圾回收過程就能夠避免。學習

從回收過程也能夠看出,一次回收過程當中,哪一個對象能被回收,也是不可控,未知的。

3.垃圾回收器對堆託管對象能夠回收,但對非託管對象就須要使用用戶手動回收。非託管對象包括文件句柄,socket,數據庫鏈接等。這類對象除了自己是託管外,其內部還包含對操做系統資源申請的對象,這類對象須要使用者主動釋放。如文件打開後,須要關閉。

非託管對象的釋放,C#提供了兩種方式。一種是使用Finalize(),一種是使用Dispose().

3.1 Finalize接口是Object的虛保護函數,C#全部的對象都繼承自Object,因此都自然能夠重寫該接口。但C#編譯器在此處作了限制,外部用戶只能改寫析構函數,編譯器編譯析構函數時,自動會加上Finalize()的調用。所以加上析構函數,就等於重寫了Finalize接口。析構函數是垃圾回收器負責調用的,那就意味着Finalize的調用,也是GC負責調用。考慮到上面GC回收對象的機制的特色,咱們能夠得出Finalize是不肯定什麼時候會調用的,雖然確定的是,其最後始終會被調用。

Finalize被GC調用的具體過程是:新對象產生時,CLI會監測到Finalize有重寫,會將對象放入一個Finalization Queue;GC發生做用時,若是該對象被檢測出能夠回收,則會先將其從Finalization Queue的引用隊列移到另一個叫freachable的引用隊列,等待下次回收時,在對象空間被釋放前,該隊列的裏對象的Finalize接口被保證調用。

Dispose()來自於接口IDispose,因此只要繼承IDispose接口,咱們就能夠重寫Dispose,在裏面來釋放非託管對象。與Finalize不一樣的是,Dispose接口的調用,須要使用者本身肯定什麼時候調用,所以該調用是明確見效的。使用具備Dispose接口的對象時,通常是採用catch,finally 的形式,在finally調用dispose,這樣確保無論出現什麼狀況dispose能夠被調用到。C#利用using關鍵字簡化了該種調用方式。(using db = new DbMyContext()){ ... }這樣離開這個using語句塊後,CLI會自動調用using新生成對象的Dispose接口。

3.2 這二者方式各有優缺點。Finalize能夠由GC確保最終會調用,非託管對象能夠被釋放,用戶能夠不用操心,但又不肯定什麼時候會釋放。Dispose能夠明確馬上釋放,但又是不可靠的,由於使用者(程序員本身)可能會忘記顯式的調用Dispose接口。

基於此,最好的辦法是利用二者的優勢,一塊兒使用。就是說在這兩個地方都來釋放,確保非託管資源必定釋放。由於Fialize的調用是垃圾回收器處理,消耗性能和時間。一旦肯定已經調用了Dispose之後,後續的Finalize是能夠不用調用的,GC.SuppressFinalize(this)能夠完成該功能來,中止Finalize的使用。

基於此,微軟給出了一種推薦的寫法:

public class MyRefObj : IDispose{

private bool disposed = false; // check whether the Dispose() has been called


//disposing is used to indicate whethet to explicitly call the managed objects' dispose

void CleanUp(bool disposing){

  if (!this.disposed)
  {
    if (disposing){

    ...  // Clean up the managed resource explicitly, like call the managed objects' Dispose

    }

    ... // Clean up the unmanaged resource

  } 

  disposed = true;

  }

public void Dispose()
{
  CleanUp(true);

// Now suppress finalization.
    GC.SuppressFinalize(this);
}

~ MyRefObj()
{
  CleanUp(false);
}
}

 

說明,析構函數中的CleanUP參數不能爲true,由於CleanUp裏面會對一些託管資源調用Dispose接口,而託管資源什麼時候被回收是不肯定的,所以這些調用的行爲是不肯定的,因此不能調用。

 

基本要點都說完了,後面在補充細節說明。

 

1. C#中的Object,Class和reference的關係

Object是Class的一個實例,而Reference是一個指針。學過C/C++的很好理解,實際其內容是一個內存地址,該內存地址裏存放的是實際的對象。

C#中全部的類的實例化都是經過引用實現,實際是使用new 來實例化一個類。以下舉例說明:

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("***** GC Basics *****");
    // Create a new Car object on the managed heap. We are returned a reference to this object ("refToMyCar").
    Car refToMyCar = new Car("Zippy", 50);
    // The C# dot operator (.) is used to invoke members on the object using our reference variable.
    Console.WriteLine(refToMyCar.ToString());
    Console.ReadLine();
  }
}

其中變量refToMyCar實際就是一個引用,一個指針,指向實際的實例對象Car("Zippy", 50)

在C#中,引用refToMyCar和實例對象Car("Zippy", 50)是存放於不一樣的內存塊中,分別叫作堆棧區和託管內存堆區;

程序的堆棧區是一個臨時數據區,存放函數調用的對象,局部變量等,函數調用完存放於堆棧區的對象的生命期就結束了,每一個線程都有固定大小的堆棧。

而堆是一個全局內存區,全部分配在Managed Heap的對象,須要管理什麼時候釋放。在C/C++語言中,這部分數據須要顯示的delete和new配套使用,而在C#中,會被.NET CLR來管理,就是說你在託管堆中只管生成新的實例,而不用管該實例佔用空間的釋放,這由CLR自動管理,實際就是垃圾回收器乾的事情。

2. 垃圾回收器(GC)

 GC是怎樣知道一個對象不在使用了呢,這涉及到後面描述的具體算法,簡而言之就是GC發現,沒法從代碼根應用到達的對象。能夠以一個不太嚴謹的解釋來理解,就是該分配在堆區的

對象,已經沒有外部引用了。以下一段代碼:

static void MakeACar()
{
  // If myCar is the only reference to the Car object, it *may* be destroyed when this
  method returns.
  Car myCar = new Car();
}

Car的對象在函數外,沒有能夠引用到,就意味着該對象能夠被回收了。

3.

C#有兩類對象,託管和非託管對象。這個託管指的是對象被誰管理,在.net框架裏,就是被CLR託管。非託管對象,指的是一些操做系統提供的資源,好比文件句柄,Socket,數據庫鏈接等。

這些資源對象的使用,須要像操做系統申請而且使用完後,及時的歸還。好比C#的FileStream類,咱們使用時有以下一段代碼:

FileInfo finfo = new FileInfo(FilePath);
//以打開或者寫入的形式建立文件流
using (FileStream fs = finfo.OpenWrite())
...{
//根據上面建立的文件流建立寫數據流
StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Default);
//把新的內容寫到建立的HTML頁面中
sw.WriteLine(strhtml);
sw.Flush();
sw.Close();
}

其中,finfo, fs, sw三個分別爲FileInfo,FileStream,StreamWriter的對象。

相關文章
相關標籤/搜索