电竞比分网-中国电竞赛事及体育赛事平台

分享

C# 內(nèi)存管理

 蘭亭文藝 2019-07-15

鏈接:https://www.cnblogs.com/kiba/p/10971744.html

前言

托管內(nèi)存與非托管內(nèi)存

托管內(nèi)存

C#語言開發(fā)的程序所使用的內(nèi)存,我們稱之為托管內(nèi)存。那么什么是托管內(nèi)存呢?我們可以先理解為,C#專用內(nèi)存;即當(dāng)C#的程序運(yùn)行起來,會向電腦內(nèi)存申請一塊專用的內(nèi)存區(qū),而這塊內(nèi)存區(qū),就叫做托管內(nèi)存。

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

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

既然是管理,那就肯定有個管理工具,那么,托管內(nèi)存的管理工具是什么呢?

GC——控制系統(tǒng)垃圾回收器,這個就是托管內(nèi)存的管理工具了,他是專門管理內(nèi)存回收的,這里就不過多的講解GC了,有興趣的朋友可以參考下面的網(wǎng)址。

參考網(wǎng)址:

GC——控制系統(tǒng)垃圾回收器 

https://docs.microsoft.com/zh-cn/dotnet/api/system.gc

弱引用 WeakReference

https://docs.microsoft.com/en-us/dotnet/api/system.weakreference

非托管內(nèi)存

既然,C#語言開發(fā)的程序所使用的內(nèi)存,都叫托管內(nèi)存,那么非托管內(nèi)存自然就是C#程序不使用的內(nèi)存了。

那么,C#程序不使用的內(nèi)存,有什么用呢?我們?yōu)槭裁匆獙W(xué)習(xí)呢?

因為,很多語言并不像C#這么優(yōu)秀,有專門的內(nèi)存管理機(jī)制,比如C++;所以,他們的變量和常量都是存儲在非托管內(nèi)存區(qū)的(對于很多語言而言,并沒有托管內(nèi)存和非托管內(nèi)存之分,他們就一個內(nèi)存,在內(nèi)存中找個地址,然后存儲數(shù)據(jù))。

所以,當(dāng)我們在做項目遇到要和其他語言進(jìn)行交互時,就要接觸非托管內(nèi)存了,因為很多時候,我們需要從非托管內(nèi)存中獲取一些的變量,或者向非托管內(nèi)存中寫入一些數(shù)據(jù)供其他語言調(diào)用。

因此,從理論上來講,C#語言對內(nèi)存的管理是最復(fù)雜的,遠(yuǎn)大于C++,因為它不僅自己開辟了一塊內(nèi)存專區(qū),同時又兼顧著控制專區(qū)外的內(nèi)存。

下圖為托管內(nèi)存與非托管內(nèi)存的關(guān)系。

安全代碼與非安全代碼

安全代碼

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

非安全代碼

非安全代碼顯然是與安全代碼相對的,即非安全代碼的變量所使用的內(nèi)存都在非托管內(nèi)存區(qū)。

因為常規(guī)狀態(tài)下我們寫的代碼都是安全代碼,所以想寫非安全代碼一定要加個特殊標(biāo)記,那就是unsafe。

unsafe
{
}

如上述代碼,在unsafe的區(qū)域內(nèi),我們就可以編寫非安全代碼。

但C#項目在默認(rèn)的情況下是不支持非安全代碼的,即當(dāng)我們嘗試些unsafe時,編譯器會報錯。為什么不默認(rèn)不允許我們使用非安全代碼呢?很簡單因為它不安全嘛。

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

默認(rèn)情況下,【允許不安全代碼】是非勾選狀態(tài);當(dāng)我們勾選上之后,編譯器就允許我們使用unsafe了。

那么,在unsafe區(qū)間如何控制非托管區(qū)域的內(nèi)存呢?

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

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

指針(Pointer)與句柄(IntPtr)

作為C#開發(fā),我們要知道【宏】和【指針】會嚴(yán)重擾亂代碼的脈絡(luò),在開發(fā)中一定要盡量避免使用。

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

但在職場中,這些我們要盡量避免使用的東西,卻是最被經(jīng)常談?wù)摰闹R點(diǎn),因為現(xiàn)在任何大學(xué)都會教C語言,所以,不論你的同事是程序員還是非技術(shù)人員,他們都多少聽過指針。而且【不會指針就不能算好程序員】幾乎已經(jīng)是一個職場準(zhǔn)則了。

因此,盡管C#開發(fā)不用這部分內(nèi)容,也一定要了解起來,不能授人以柄不是嘛。

指針(Pointer)

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

在C#中,我們也是可以定義指針的,不過那需要在非安全代碼內(nèi)定義;因為指針直接從內(nèi)存中獲取地址的,也就是說,它并不是通過C#的內(nèi)存管理工具來開辟內(nèi)存的,所以,指針申請的這塊內(nèi)存并不在托管代碼的內(nèi)存區(qū)中,那么,很自然的,這塊內(nèi)存就在非托管代碼的內(nèi)存區(qū)中了。

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

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();

上述代碼非常簡單,我先將字符串發(fā)送給MarshalHelper幫助類轉(zhuǎn)換成句柄(MarshalHelper中會開辟一個非托管區(qū)內(nèi)存空間,然后把托管區(qū)的字符串str的值賦值到這個非托管區(qū)內(nèi)存,再生成一個指針指向這塊內(nèi)存,最后在將這個指針轉(zhuǎn)換成IntPtr句柄,當(dāng)然描述起來很復(fù)雜其實(shí)也就一句話Marshal.StringToHGlobalAnsi(str))然后調(diào)用轉(zhuǎn)換出來的句柄的ToPointer方法獲取到指針,接著在在非全代碼區(qū)域使用指針輸出它的內(nèi)容,再修改該它的值,最后將修改后值的指針內(nèi)容打印出來。

PS:代碼中的MarshalHelper是我封裝的一個類,用于處理類型與IntPtr的轉(zhuǎn)換,下方github中有該類代碼。

其實(shí)指針在C#中有意義的功能就只剩下內(nèi)存偏移量調(diào)整了,但實(shí)際開發(fā)中,C#項目是不需要做內(nèi)存偏移量調(diào)整這種操作的。所以,純C#項目幾乎可以說已經(jīng)棄用指針了。

句柄(IntPtr)

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

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

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

從圖中我們可以看到,句柄IntPtrt里包含創(chuàng)建指針,獲取指針長度,設(shè)置偏移量等等方法,并且為了編碼方便還聲明了些強(qiáng)制轉(zhuǎn)換的方法。

看了句柄的結(jié)構(gòu)體定義,相信稍微有點(diǎn)基礎(chǔ)的人已經(jīng)明白了,在C#中,微軟是希望拋棄指針而改用更優(yōu)秀的句柄代替它的。

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

這是因為,在項目開發(fā)中總是會有極特殊的情況,比如,你有一段C++寫的非常復(fù)雜、完美的函數(shù),而將這個函數(shù)轉(zhuǎn)換成C#又及其耗時,那么最簡單省力的方法就是直接在C#里啟用指針進(jìn)行移植。

也就是說,C#支持指針,其實(shí)是為了體現(xiàn)它的兼容性,并不是提倡大家去使用指針。

內(nèi)存釋放

我先看如下代碼:

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;
}

代碼中有兩個函數(shù)Int32ToIntPtr_Free和Int32ToIntPtr_NoFree,兩個函數(shù)都是將變量testint轉(zhuǎn)換成指針,然后返回該指針的地址(int類型),區(qū)別是一個調(diào)用了MarshalHelper.Free(pointerInt)進(jìn)行指針內(nèi)存釋放,一個沒有調(diào)用。

兩個函數(shù)執(zhí)行完成后,開啟線程,通過其返回的指針的地址,在重新查找指針對應(yīng)的內(nèi)容,結(jié)果如下圖:

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

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

結(jié)語

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

到此,C#內(nèi)存管理講解就結(jié)束了。 

代碼已經(jīng)傳到Github上了,歡迎大家下載。

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

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多