第5章 託管和非託管的資源

  • 1.什麼是託管與非託管?

託管資源:通常是指被CLR(公共語言運行庫)控制的內存資源,這些資源由CLR來管理。能夠認爲是.net 類庫中的資源。數據庫

非託管資源:不受CLR控制和管理的資源。數組

對於託管資源,GC負責垃圾回收。對於非託管資源,GC能夠跟蹤非託管資源的生存期,可是不知道如何釋放它,這時候就要人工進行釋放。安全

2. 後臺內存管理服務器

介紹給變量分配內存時在計算機的內存中發生的狀況網絡

(1) 值數據類型:架構

  • 不是對象成員的值數據類型採用棧存儲方式。
  • 棧指針表示棧中下一個空閒存儲單元的地址。
  • 棧存儲是從高內存地址向低內存地址填充。
  • 值數據變量超出做用域時,CLR就知道再也不須要這個變量。釋放變量時,其順序老是與給它們分配內存的順序相反。

(2) 引用數據類型:函數

  • 用new運算符請求的內存空間(即託管堆)。用於存儲一些數據,在方法退出後很長一段時間內數據仍可用。
  • 託管堆是在垃圾回收器GC的控制下工做。
  • 堆工做原理及內存分配:
void DoWork()
{
     Customer arabel;           // 在棧上給這個應用分配存儲空間,但僅是一個引用,佔用4個字節的空間,而不是實際的Customer對象。
     arabel = new Customer();   // 首先分配堆上的內存,以存儲Customer對象;再把變量arabel的值設置爲分配給新Customer對象的內存地址。
     // Customer實例沒有放在棧中,而是放在堆中
     // 與棧不一樣,堆上的內存是向上分配的
//------------------------------------------------------------------ Customer otherCustomer2 = new EnhancedCustomer(); //用一行代碼在棧上爲otherCustomer2引用分配空間,同時在堆上爲EnhancedCustomer對象分配空間。 }
  • 引用變量賦值:把一個引用變量的值賦予另外一個相同類型的變量,則有兩個變量引用內存中的同一個對象。
  • 引用變量回收:當一個引用變量超出做用域是,它會從棧中刪除引用,但引用對象的數據仍保留在堆中;一直到程序終止,或GC刪除它爲止;只有在該數據再也不被任何變量引用時,它纔會被刪除。

(3) 垃圾回收性能

  • GC運行時,會從堆中刪除再也不引用的全部對象。
  • GC壓縮操做:對於託管堆,在刪除了能釋放的全部對象後,就會把其餘對象移動回堆的頂端,再次造成一個連續的內存塊。
  • 堆系列:
    • 第0代:建立新對象時,會把它們移動到堆的這個部分中;駐留最新的對象;對象會繼續放在這個部分,知道垃圾回收過程第一次進行回收。
    • 第1代:第一次回收清理過程以後,仍保留的對象會被壓縮,並移動到該部分。此時第0代對應的部分爲空。
    • 第2代:對於第1代中的老對象,這樣的移動會再次發生,則其遺留下來的對象會移動到堆的第2代。位於第0代的對象會移動到第1代,第0代仍用於放置新對象。
    • 在給對象分配內存空間時,若是超出了第0代對應的部分的容量,或調用GC.Collect()方法,就會進行垃圾回收。
  • 有助於提升性能的領域:
    • 大對象堆(大於85 000個字節的對象):架構處理堆上較大對象的方式,其對象不執行壓縮過程。第2代和大對象堆的回收放在後臺線程上進行,即應用程序線程僅會爲第0代和第1代的回收而阻塞,從而減小了總暫停時間。
    • 垃圾回收的平衡:專用於服務器的垃圾回收。平衡小對象堆和大對象堆,能夠減小沒必要要的回收。

3. 強引用和弱引用優化

  • 強引用:GC不能回收仍在引用的對象的內存。
    • 缺點是強引用實例超出做用域,或指定爲null。若是GC在運行,很容易錯過引用的清理,不能釋放引用的內存。可以使用WeakReference避免這種狀況。
  • 弱引用:使用WeakReference類建立。使用構造函數,能夠傳遞強引用(Target屬性)。
    • 弱引用對象可能在任意時刻被回收,所以引用該對象前必須確認存在(IsAlive屬性爲true)。成功檢索強引用後,能夠經過正常方式使用它。
//建立一個DataObject,並傳遞構造函數返回的弱引用
var myWeakReference = new WeakReference(new DataObject());

if (myWeakReference.IsAlive)
{
    DataObject strongReference = myWeakReference.Target as DataObject;
    if (strongReference != null)
    {
         //使用強引用對象 strongReference
    }    
}
else
{
     // 引用不可用
}

 

4. 處理非託管的資源ui

  • GC不知道如何釋放非託管資源(如文件句柄、網絡鏈接和數據庫鏈接)
  • 託管類在封裝對非託管資源的直接或間接引用時,需制定專門的規則,確保非託管的資源在回收類的一個實例時釋放。
  • 自動釋放非託管資源的機制:
    • 聲明一個析構函數(或終結器),做爲類的一個成員
    • 在類中實現System.IDisposable接口

(1) 析構函數或終結器

  • 在C#中定義析構函數時,編譯器發送給程序集的其實是Finalize()方法
  • C#析構函數問題
    • 不肯定性:因爲GC的工做方式,沒法肯定C#對象的析構函數什麼時候執行。
    • 會延遲對象最終從內存中刪除的時間。
      • 沒有析構函數的對象:在GC的一次處理中從內存刪除
      • 有析構函數的對象:須要兩次才能銷燬。

(2) IDisposable接口:推薦使用,爲釋放非託管的資源提供了肯定的機制,精確控制什麼時候釋放資源。

  • 通常方式
SqlConnection conn = null;
try
{
    conn = new SqlConnection();
    //do something;
}
finally
{
    conn?.Dispose();
}

 

  • 改進方式,使用using簡化輸入,編譯器自動翻譯成 try...finally。在變量超出做用域是,會自動調用其Dispose()方法。
using(SqlConnection conn = new SqlConnection())
{
      //do something;
}

 

(3) 雙重實現:正確調用Dispose(),同時將析構函數做爲一種安全機制。

    public class BaseResource : IDisposable
    {
        private IntPtr _handle; // 句柄,屬於非託管資源
        private System.ComponentModel.Component _comp; // 組件,託管資源
        private bool _isDisposed = false; // 是否已釋放資源的標誌

        //實現接口方法
        //由類的使用者,在外部顯示調用,釋放類資源
        public void Dispose()
        {
            Dispose(true);// 釋放託管和非託管資源

            // 將對象從垃圾回收器鏈表中移除,
            // 從而在垃圾回收器工做時,只釋放託管資源,而不執行此對象的析構函數
            GC.SuppressFinalize(this);
        }

        //由垃圾回收器調用,釋放非託管資源
        ~BaseResource()
        {
            Dispose(false);// 釋放非託管資源
        }

        //參數爲true表示釋放全部資源,只能由使用者調用
        //參數爲false表示釋放非託管資源,只能由垃圾回收器自動調用
        //若是子類有本身的非託管資源,能夠重載這個函數,添加本身的非託管資源的釋放
        //可是要記住,重載此函數必須保證調用基類的版本,以保證基類的資源正常釋放
        protected virtual void Dispose(bool disposing)
        {
            if (!this._isDisposed)// 若是資源未釋放 這個判斷主要用了防止對象被屢次釋放
            {
                if (disposing)
                {
                    // 釋放託管資源,調用其Dispose方法 
                    _comp.Dispose();
                }    

                // 釋放非託管資源
                closeHandle(_handle);
                _handle= IntPtr.Zero;         
            }
            this._isDisposed = true; // 標識此對象已釋放
        }
    }

 

5.不安全的代碼:C#直接訪問內存

(1) 用指針直接訪問內存:

  • 引用就是一個類型安全的指針。
  • 使用指針能夠訪問實際內存地址,執行新類型的操做。
  • 使用指針的緣由:
    • 向後兼容性:用於調用本地的Windows API => 可以使用DllImport聲明,以免使用指針。
    • 性能:指針提供最優速度性能 => 可以使用代碼配置文件,查找代碼中的瓶頸。
  • 使用指針,必須授予代碼運行庫的代碼訪問安全機制的高級別信任。
  • 強烈建議不要輕易使用指針。

 (2) 用unsafe關鍵字編寫不安全的代碼

  • 方法、方法的參數、類或結構、成員,都可使用unsafe
  • 方法中的一塊代碼可標記爲unsafe
void MyMethod()
{
     // code that doesn't use pointers
     unsafe
     {
           // unsafe code that uses pointers here
     }
     // more 'safe' code that doesn't use pointers
}
  •  不能把局部變量自己標記爲unsafe

(3) 指針的語法

  • 把代碼塊標記爲unsafe後,可使用指針語法聲明。
  • 指針運算符
    • & 尋址運算符:表示「取地址」,並把一個值數據類型轉換爲指針。
    • * 間接尋址運算符:表示「獲取地址的內容」,把一個指針轉換爲值數據類型。
  • 指針能夠聲明爲任意一種值類型,包括結構;但不能聲明爲類或數組。

(4) 將指針強制轉換爲整數類型

  • 因爲指針實際上存儲了一個表示地址的整數,所以任何指針中的地址均可以和任何整數類型之間相互轉換。
  • 轉換必須是顯示指定的。
  • 轉換的主要目的是顯示指針地址
  • 32位系統,可轉換爲uint、long、ulong
  • 64位系統,可轉換爲ulong

(5) 指針類型之間的強制轉換

  • 能夠在指向不一樣類型的指針之間進行顯式轉換
  • 指針類型之間轉換,實現C union類型的等價形式

(6) void指針

  • 維護一個指針,但不但願指定它指向的數據類型,可聲明爲void指針
  • 主要用途:調用須要void*參數的API函數

(7) 指針算術運算

  • 指針加減整數,是指指針存儲地址值的變化。
  • 不一樣類型的指針字節數不一樣
    • 給類型爲T的指針加上數值X,其中指針的值爲P,則獲得的結果是P+X*(sizeof(T))
    • 若是類型是byte或char,總字節數不是4的倍數,連續值不是默認地存儲在連續的存儲單元中
  • 對指針使用+、-、+=、-=、++、--,其右邊變量必須是long或ulong
  • 不容許對void指針執行算術運算

(8)sizeof運算符:肯定各類數據類型的大小。

  • 優勢是沒必要在代碼中硬編碼數據類型的大小
sizeof(char) = 2; sizeof(bool) = 1;

 

  • 結構可使用,類不能使用。

(9)結構指針:指針成員訪問運算符

  • 結構不能包含任何引用類型,由於指針不能指向任何引用類型。
  • 指針成員訪問運算符: ->
// 結構
struct MyStruct
{
     public long X;
     public float F;  
}

// 結構指針
MyStruct* pStruct;

// 初始化
var myStruct = new MyStruct();
pStruct = &myStruct;

// 經過指針訪問結構的成員值
(*pStruct).X = 4;
(*pStruct).F = 3.4f;
// 使用成員訪問運算符
pStruct->X = 4;
pStruct->F = 3.4f;

//指針指向結構中一個字段
long* pL = &(pStruct->X);
float* pF = &(pStruct->F);

 

(10) 類成員指針

  • 對類中值類型成員建立指針,須要使用fixed關鍵字,告知GC這些對象不能移動。
    • 在執行fixed塊中代碼時,不能移動對象位置。
    • 聲明多個fixed指針,就能夠在同一個代碼塊錢放置多條fixed對象
    • 能夠嵌套fixed塊
    • 類型相同,可在同一個fixed塊中初始化多個變量

(11)使用指針優化性能:建立基於棧的數組。

  • 在棧中的數組具備高性能、低系統開銷的特色,但只對於一位數組比較簡單。
  • 關鍵字stackalloc:指示在棧上分配必定量的內存。
    • 要存儲的數據類型
    • 要存儲的數據項數
int size; 
size = 20; 
double* pDoubles = stackalloc double[size];
    • 分配的字節數是項數乘以sizeof(數據類型)
    • stackalloc返回的指針指向新分配內存塊的頂部
    • 數組元素訪問:
      • 使用指針算術,即表達式*(pDouble+X)訪問數組中下標爲X的元素
      • 表達式pDouble[X]在編譯時解釋爲*(pDouble+X)

6.平臺調用

  •  並非Windows API調用的全部特性均可用於.NET Framework,可採用平臺調用方法實現。
  • 採用extern修飾符標記
  • 用屬性[DllImport]引用DLL
  • 非託管方法定義的參數類型必須用託管代碼映射類型
  • C++有不一樣Boolen數據類型,可以使用特性[MarshalAs]指定.NET類型bool應映射爲哪一個本機類型
// C++ 調用Windows API(kernel32.dll)中CreateHardLink
BOOL CreateHardLink(
       LPCTSTR lpFileName,
       LPCTSTR lpExistingFileName,
       LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

// C# 調用CreateHardLink
[DllImport("kernel32.dll", SetLastError="true",
                     EntryPoint="CreateHardLink", CharSet=CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateHardLink(string newFileName,
            string existingFilename,
            IntPtr securityAttributes);

 

  • 一般本地方法時,一般必須使用Windows句柄(IntPtr結構);NET2.0引入SafeHandle類,派生的句柄類型是SafeFileHandle\SafeWaitHandle\SafeNCryptHandle\SafePipeHandle。
相關文章
相關標籤/搜索