【C#】內存管理

轉自:https://www.cnblogs.com/kiba/p/10971744.html

託管內存與非託管內存

託管內存

C#語言開發的程序所使用的內存,我們稱之爲託管內存。那麼什麼是託管內存呢?我們可以先理解爲,C#專用內存;即當C#的程序運行起來,會向電腦內存申請一塊專用的內存區,而這塊內存區,就叫做託管內存。

在C#語言開發的程序中,我們所聲明的變量,不論是常量,還變量,都在這塊內存中。即,我們聲明一個int k或是聲明一個對象 new Class,他們都是在這塊內存中的。

而這塊內存(託管內存),它很特別,它自身是帶管理功能的,即,它自己會判斷,你聲明的內存還用不用,不用他就給回收了。

既然是管理,那就肯定有個管理工具,那麼,託管內存的管理工具是什麼呢?

GC——控制系統垃圾回收器,這個就是託管內存的管理工具了,他是專門管理內存回收的,這裏就不過多的講解GC了,有興趣的朋友可以參考下面的網址。

參考網址:

GC——控制系統垃圾回收器 

弱引用 WeakReference

非託管內存

既然,C#語言開發的程序所使用的內存,都叫託管內存,那麼非託管內存自然就是C#程序不使用的內存了。

那麼,C#程序不使用的內存,有什麼用呢?我們爲什麼要學習呢?

因爲,很多語言並不像C#這麼優秀,有專門的內存管理機制,比如C++;所以,他們的變量和常量都是存儲在非託管內存區的(對於很多語言而言,並沒有託管內存和非託管內存之分,他們就一個內存,在內存中找個地址,然後存儲數據)。

所以,當我們在做項目遇到要和其他語言進行交互時,就要接觸非託管內存了,因爲很多時候,我們需要從非託管內存中獲取一些的變量,或者向非託管內存中寫入一些數據供其他語言調用。

因此,從理論上來講,C#語言對內存的管理是最複雜的,遠大於C++,因爲它不僅自己開闢了一塊內存專區,同時又兼顧着控制專區外的內存。

下圖爲託管內存與非託管內存的關係。

安全代碼與非安全代碼

安全代碼

C#的安全代碼就是C#日常寫的代碼,其特點就是代碼中聲明的變量都在託管內存;而之所以叫安全代碼,則是因爲內存全部託管給了內存管理器,不存在內存泄漏的問題(當然,這是理論上,實際情況某些微軟的控件還是存在內存泄漏的問題,相信一定有人遇到過,不過99%的情況下是沒問題的)。

非安全代碼

非安全代碼顯然是與安全代碼相對的,即非安全代碼的變量所使用的內存都在非託管內存區。

因爲常規狀態下我們寫的代碼都是安全代碼,所以想寫非安全代碼一定要加個特殊標記,那就是unsafe。

unsafe
{

}


但C#項目在默認的情況下是不支持非安全代碼的,即當我們嘗試些unsafe時,編譯器會報錯。爲什麼不默認不允許我們使用非安全代碼呢?很簡單因爲它不安全嘛。如上述代碼,在unsafe的區域內,我們就可以編寫非安全代碼。

想啓用C#的非安全代碼設置也很簡單,右鍵項目—屬性—生成,如下圖所示:

默認情況下,【允許不安全代碼】是非勾選狀態;當我們勾選上之後,編譯器就允許我們使用unsafe了。

那麼,在unsafe區間如何控制非託管區域的內存呢?

這就需要使用到指針了,下面我們講一下C#中的指針。

注意:非安全代碼並不是C#的主要功能,而是爲了兼容其他使用非託管內存的語言而存在的,所以即便你不瞭解也並不會影響你的技術水平,但在職場中,這塊的內容非常容易成爲菜鳥攻擊你的利器,所以學會它是職場生存的重要手段之一。

指針(Pointer)與句柄(IntPtr)

作爲C#開發,我們要知道【宏】和【指針】會嚴重擾亂代碼的脈絡,在開發中一定要儘量避免使用。

比如,你定義了一個Void*的指針,那Void*到底是個什麼東西啊!沒人知道,因爲它什麼都能指向,很明顯,這嚴重的影響了代碼的正常閱讀,因爲我需要讀到Void*的時候,還有調查下它是個什麼東西;但我們又不是在看論文,看到特有名詞還得查一下他的含義,這簡直太荒唐了。

但在職場中,這些我們要儘量避免使用的東西,卻是最被經常談論的知識點,因爲現在任何大學都會教C語言,所以,不論你的同事是程序員還是非技術人員,他們都多少聽過指針。而且【不會指針就不能算好程序員】幾乎已經是一個職場準則了。

因此,儘管C#開發不用這部分內容,也一定要了解起來,不能授人以柄不是嘛。

指針(Pointer)

指針簡單來說就是指向一塊內存的內存,我們可以通過指針指向的內存地址找到變量的值,並且改變它。

在C#中,我們也是可以定義指針的,不過那需要在非安全代碼內定義;因爲指針直接從內存中獲取地址的,也就是說,它並不是通過C#的內存管理工具來開闢內存的,所以,指針申請的這塊內存並不在託管代碼的內存區中,那麼,很自然的,這塊內存就在非託管代碼的內存區中了。

下面我們先看這樣一段代碼,來了解一下指針:

string str = "I am Kiba518!";
int strlen = str.Length;
IntPtr sptr = MarshalHelper.StringToIntPtr(str);
unsafe
{
    char* src = (char*)sptr.ToPointer();
    //Console.WriteLine("地址" + (&src)); //這樣寫會報錯,C#並不支持這樣取指針地址
    for (int i = 0; i <= strlen; i++)
    {
        Console.Write(src[i]);
        src[i] = '0';
    }
    Console.WriteLine();
    Console.WriteLine("========不安全代碼改值=========");
    for (int i = 0; i <= strlen; i++)
    {
        Console.Write(src[i]);
    }
}
Console.ReadKey();

PS:代碼中的MarshalHelper是我封裝的一個類,用於處理類型與IntPtr的轉換,下方github中有該類代碼。上述代碼非常簡單,我先將字符串發送給MarshalHelper幫助類轉換成句柄(MarshalHelper中會開闢一個非託管區內存空間,然後把託管區的字符串str的值賦值到這個非託管區內存,再生成一個指針指向這塊內存,最後在將這個指針轉換成IntPtr句柄,當然描述起來很複雜其實也就一句話Marshal.StringToHGlobalAnsi(str))然後調用轉換出來的句柄的ToPointer方法獲取到指針,接着在在非全代碼區域使用指針輸出它的內容,再修改該它的值,最後將修改後值的指針內容打印出來。

----------------------------------------------------------------------------------------------------

其實指針在C#中有意義的功能就只剩下內存偏移量調整了,但實際開發中,C#項目是不需要做內存偏移量調整這種操作的。所以,純C#項目幾乎可以說已經棄用指針了。

句柄(IntPtr)

句柄其實是一個指針的封裝,同樣的,它也不常用,因爲C#項目中指針都被棄用了,那指針的封裝—句柄自然也被棄用了。

但總有特殊的地方會用到指針,比如調用C++動態庫之類的;所以微軟貼心的爲我們做了個句柄,畢竟指針用起來太難受了。

句柄是一個結構體,簡單的來說,它是指針的一個封裝,是C#中指針的替代者,下面我們看下句柄的定義。

從圖中我們可以看到,句柄IntPtrt裏包含創建指針,獲取指針長度,設置偏移量等等方法,並且爲了編碼方便還聲明瞭些強制轉換的方法。

看了句柄的結構體定義,相信稍微有點基礎的人已經明白了,在C#中,微軟是希望拋棄指針而改用更優秀的句柄代替它的。

但我們還會發現,句柄裏還提供一個方法是ToPointer(),它的返回類型是Void*,也就是說,我們還是可以從句柄裏拿到C++中的指針,既然,微軟期望在C#中不要使用指針,那爲什麼還要提供這樣的方法呢?

這是因爲,在項目開發中總是會有極特殊的情況,比如,你有一段C++寫的非常複雜、完美的函數,而將這個函數轉換成C#又及其耗時,那麼最簡單省力的方法就是直接在C#裏啓用指針進行移植。

也就是說,C#支持指針,其實是爲了體現它的兼容性,並不是提倡大家去使用指針。

內存釋放

我先看如下代碼:

static void Main(string[] args)
{
    int retNoFree = Int32ToIntPtr_NoFree();
    IntPtr retNoFreeIP = new IntPtr(retNoFree);
    int retFree = Int32ToIntPtr_Free();
    IntPtr retFreeIP = new IntPtr(retFree);

    new Task(() =>
    {
        int afterNoFree = MarshalHelper.IntPtrToInt32(retNoFreeIP);
        Console.WriteLine("Int32ToIntPtr_NoFree-未釋放Intptr的線程取值" + afterNoFree);
        int afterFree = MarshalHelper.IntPtrToInt32(retFreeIP);
        Console.WriteLine("Int32ToIntPtr_Free-已釋放Intptr的線程取值" + afterFree);
    }).Start();
    Console.ReadKey();
}

static int Int32ToIntPtr_Free()
{
    IntPtr pointerInt = new IntPtr();
    int testint = 518;
    pointerInt = MarshalHelper.Int32ToIntPtr(testint);
    int testintT = MarshalHelper.IntPtrToInt32(pointerInt);
    Console.WriteLine("Int32ToIntPtr_Free-取IntPtr的值" + testintT);
    MarshalHelper.Free(pointerInt);
    int testintT2 = (int)pointerInt;
    return testintT2;
}

static int Int32ToIntPtr_NoFree()
{
    IntPtr pointerInt = new IntPtr();
    int testint = 518;
    pointerInt = MarshalHelper.Int32ToIntPtr(testint);
    int testintT = MarshalHelper.IntPtrToInt32(pointerInt);
    Console.WriteLine("Int32ToIntPtr_NoFree-取IntPtr的值" + testintT);
    int testintT2 = (int)pointerInt;
    return testintT2;
}

兩個函數執行完成後,開啓線程,通過其返回的指針的地址,在重新查找指針對應的內容,結果如下圖:代碼中有兩個函數Int32ToIntPtr_Free和Int32ToIntPtr_NoFree,兩個函數都是將變量testint轉換成指針,然後返回該指針的地址(int類型),區別是一個調用了MarshalHelper.Free(pointerInt)進行指針內存釋放,一個沒有調用。

從圖中我們可以看到,未進行Free的IntPtr,仍然可以通過指針地址獲取到他的內容,而已釋放的IntPtr,通過地址再獲取內容,則已經是其他內容了。

PS:在C#中指針的內存釋放需要 Marshal.FreeHGlobal(IntPtr)方法,同樣的我將其封裝到了MarshalHelper中了。

結語

在職場,我們需要防備的通常不是高手,而是菜鳥,所以我們必須要增加各種各樣的知識儲備來應對這些奇奇怪怪的事情。

----------------------------------------------------------------------------------------------------

到此,C#內存管理講解就結束了。

代碼已經傳到Github上了,歡迎大家下載。

Github地址:https://github.com/kiba518/MarshalHelper

----------------------------------------------------------------------------------------------------

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章