本次日誌,我們來重點聊一聊軟體開發過程中,如何提升效能方面的問題。這是軟體開發或研發過程中深層的問題,這篇文章主要從記憶體分配和記憶體回收兩方面說明,我們軟體程式碼編寫過程中,計算如何來運作的。在此你可以了解記憶體管理的過程和方式,以便在以後的軟體開發中註意它、利用它。
值類型包括:int,float,double,bool,結構,引用,表示物件實例的變數
引用型別包括:類別與陣列;比較特殊的參考型別string、object
一般情況下:值型別儲存在堆疊中(不包括包含在引用中的值類型,如類別的值字段,類別中的引用字段,數組的元素這些都是隨引用存儲在受管制的堆中);引用類型存儲在受管制的堆中,為什麼說是受管制的堆,下面會具體來談。
幾個概念:
虛擬記憶體:32位元的計算機,每個進程擁有4G的虛擬記憶體。
受管制的堆(託管堆):受誰的管制?當然是無用單元收集器,即垃圾收集器。如何管理下面再說?
無用單元收集器:垃圾收集器除了會壓縮託管堆、更新引用位址、還會維護託管堆的資訊清單等等。
關於值類型的儲存先看如下程式碼:
{
int age=20;
double salary=2000;
}
}
分配4個位元組的記憶體空間來儲存age值,它是儲存在堆疊上的。堆疊屬入先進後出的資料結構,堆疊是從高位址到低位址儲存資料的。電腦暫存器中保持著一個堆疊指針,他總是指向堆疊最底端的自由空間位址,當我們定義一個int類型的值時,堆疊指標遞減四個位元組的位址;當變數出了作用域後,堆疊指標對應的對應的遞增四個位元組的位址,它只是堆疊指標的上下移動,所以堆疊的效能是相當高的。
下面看一下引用型別的存儲,還是先看程式碼:
{
Customer customerA;
customerA=new Customer(); //假設Customer
第一行先宣告了一個Customer引用,引用的名字為customerA,在堆疊上給此引用分配儲存空間,儲存空間的大小為4個字節,因為它只儲存了一個引用,這個引用所指向的才是即將儲存Customer實例的空間位址,注意此時customerA並沒有指向特定的空間,它只是分配了一個空間而已。
第二行執行過程中,.net環境會搜尋託管堆,尋找第一個未使用的、連續的32個位元組空間分配給類別的實例,並設定customerA指向這段空間的頂端位置(堆的空間是從低到高使用的)。當引用變數出作用域後,堆疊中的參考會無效,當託管堆中的實例還在,直到垃圾收集器對其進行清理。
到這裡細心的讀者可能會有些疑問,是不是定義引用類型時,計算機要搜索整個堆,尋找足夠大的內存空間來存儲對象呢?這樣會不會效率很低?如果沒有足夠大的連續的空間呢?這個就要談到「託管」了。堆是受垃圾收集器管理的,.net在執行垃圾收集器時釋放能釋放的所有對象,並壓縮其他對象,然後把所有自由空間組合在一起移動到堆的頂端, 形成連續的塊,同時更新其他移動物件的引用。如果再有物件定義,可以很快找到適合的空間。如此看來託管堆工作方式與堆疊類似,它是透過堆疊指標來完成空間的分配和回收的。
上面談了.net對記憶體空間分配的管理流程和方式,接下來談一談對記憶體的回收過程。談到資源的清理,不得不提的兩個概念和三個方法。
兩個概念為:託管資源和非託管資源。
託管資源接受.net framework的CLR(通用語言運行時)的管理;非託管資源則不受它的管理。
三個方法為:Finalize(),Dispose(),Close()。
一、Finalize()為析構方法,清除非託管資源。
在類別中定義方式:
public ClassName{
~ClassName()
{
//清理非託管資源(如關閉檔案和資料庫聯接等)
}}
}}
}}
}}
}是:1. 運作不確定性。 它是受垃圾收集器的管理,當垃圾收集器工作時,會呼叫此方法。 2. 性能開銷大。 垃圾收集器工作方式為,物件如果執行了Finalize()方法,垃圾收集器第一次執行時,會把它放在一個特殊的佇列中;第二次執行的時候才會刪除此物件。 3. 不能顯示定義和調用,定義為析構方法形式。 🎜🎜是基於上述特點,最好不要執行Finalize()方法,除非類別確實需要它或與其他兩個方法結合。 🎜🎜二、Dispose()方法,可清除一切需要清除的資源,包括託管和非託管資源。 🎜🎜定義如下:🎜public void Dispose()
{
//清理應該清理的資源(包括託管和非託管資源)
System.GC.SuppressFinalize(this); //這句話很重要,以下會解釋原因。
}
它的特點:
1. 任何客戶程式碼都應顯示呼叫這個方法,來釋放資源。
2. 由於第一點的原因,一般要做一個備份,這個備份一般由析構方法來擔任角色。
3. 定義此方法的類,必須繼承IDisposable介面。
4. 語法關鍵字using等同於呼叫Dispose(),使用using時,它是預設呼叫Dispose()方法。所以使用using的類別也要繼承IDisposable介面。
Dispose()方法比較靈活,在資源不需要時立即釋放。它是資源的最終處理,呼叫它意味著最終會刪除物件。
三、Close()方法,暫時處置資源的狀態,可能以後還會使用。一般處理非託管資源。
定義如下:
public viod Close()
{
//對非託管資源狀態的設置,如關閉文件或資料庫連接
}
它的特徵:
設置,一般是關閉文件或資料庫連接。}
protected virtual void Dispose(bool disposing)
if (disposing)
{
}
// 這裡是清理"非託管資源"的使用者程式碼段。此處為析構方法的實際執行程式碼,為了避免客戶程式碼忘記顯示呼叫Dispose()方法,所做的備份。
}
~ResourceHolder()
{
Dispose(false); // 這裡是清理"非託管資源"
}
}}
一定要仔細閱讀以下解釋,很經典,不看會後悔呦。
這裡,我們必須要清楚,需要用戶調用的是方法Dispose()而不是方法Dispose(bool),然而,這裡真正執行釋放工作的方法卻並不是Dispose(),而是Dispose(bool) ! 為什麼呢?仔細看程式碼,在Dispose()中,調用了Dispose(true),而參數為"true"時,作用是清理所有的託管資源和非託管資源;大家一定還記得我前面才說過,"使用析構方法是用來釋放非託管資源的",那麼這裡既然Dispose()可以完成釋放非託管資源的工作,還要析構方法幹什麼呢? 其實,析構方法的作用僅僅是一個"備份" !
為什麼呢?
格地說,凡執行了介面"IDisposable"的類別,那麼只要程式設計師在程式碼中使用了這個類別的物件實例,那麼早晚得呼叫這個類別的Dispose()方法,同時,如果類別中含有對非託管資源的使用,那麼也必須釋放非託管資源! 可惜,如果釋放非託管資源的程式碼放在析構方法中(上面的例子對應的是" ~ResourceHolder() "),那麼程式設計師想呼叫這段釋放程式碼是不可能做到的(因為析構方法不能被用戶呼叫,只能被系統,確切地說是"垃圾回收器"調用),所以大家應該知道為什麼上面例子中"清理非託管資源的用戶代碼段"是在Dispose(bool)中,而不是~ResourceHolder()中! 不過不幸的是,並不是所有的程式設計師都時刻小心地記得呼叫Dispose()方法,萬一程式設計師忘記呼叫此方法,託管資源當然沒問題,早晚會有"垃圾回收器"來回收(只不過會推遲一會兒),那麼非託管資源呢?它可不受CLR的控制啊!難道它所佔用的非託管資源就永遠不能釋放了嗎? 當然不是!我們還有"析構方法"呢! 如果忘記調用Dispose(),那麼"垃圾回收器"也會調用"析構方法"來釋放非託管資源的!(多說一句廢話,如果程式設計師記得呼叫Dispose()的話,那麼程式碼"System.GC.SuppressFinalize(this);"則可以防止"垃圾回收器"呼叫析構方法,這樣就不必多釋放一次"非託管資源"了) 所以我們就不怕程式設計師忘記呼叫Dispose()方法了。