最近在做一個專案,用到了大量的非託管技術,所以垃圾回收變得很重要。
在說垃圾回收之前,先說說兩個概念:
- 託管程式碼,是由CLR管理的程式碼
- 非託管程式碼,是由作業系統直接執行的程式碼
在早期C++的時候,記憶體分配和釋放都是由我們手動處理的,而在公共語言進行時CLR中,多了一個垃圾收集器GC,來充當自動記憶體管理器,完成同樣的工作。從此,對於開發人員來說,我們可以不需要用顯式的程式碼來執行記憶體管理。這樣做的好處是明顯的:大量相關記憶體的錯誤被消除了,比方沒有釋放物件導致的記憶體洩露,或試圖訪問已經釋放的物件的記憶體,等等。
為了防止不提供原網址的轉載,特在這裡加上原文連結:https://www.cnblogs.com/tiger-wang/p/14469069.html
一、回收和管理託管資源
上面說了,垃圾回收GC在Dotnet中是一個自動的記憶體管理器,是一種機制,用來清理和回收堆記憶體中未引用的部分。
通常CLR會在這些情況下啟動垃圾回收:
- 需要在堆上分配記憶體給一個新物件,但沒有足夠的空閒記憶體時;
- 物件被強制Dispose時;
- 託管堆上已分配物件的記憶體超過了閥值(這個閥值會動態調整);
- 呼叫了
GC.Collect
方法
這些內容都是基礎,瞭解了非常好,面試時有話可說。不瞭解也沒關係,不會影響做一個好的程式出來。
下面的內容如果能記住,倒是對於程式開發很有幫助。
在Dotnet的垃圾回收機制中,回收器會自行優化並適用於多種方案。但是,我們仍然可以根據執行環境來設定垃圾回收的型別。
Dotnet的CLR提供了下面兩種型別的垃圾回收:
- 工作站垃圾回收
- 伺服器垃圾回收
這兩種回收機制,有一定的區別。
工作站回收,主要是為客戶端應用設計的,也是程式預設的回收機制。垃圾回收的過程,跑在觸發垃圾回收的使用者執行緒上,並使用相同的優先順序。這種方式,優點是不會被掛起或延遲,缺點是需要與其它執行緒競爭CPU時間。當執行環境中只有一個CPU時,系統會自動採用工作站方式,不管你設定成什麼。
伺服器回收,針對的是高吞吐的伺服器應用,回收過程跑在專用的高優先順序執行緒上,而且預設是多執行緒在跑,所以效率更高,缺點是佔用的資源會更多,而且由於執行緒之間的干擾和上下文切換,會影響整體效能。
所以,選擇什麼樣的回收機制,需要認真分析。通常普通應用,工作站回收就好。如果是伺服器端的API服務,需要選擇伺服器回收。而如果是在服務端需要啟動多個例項進行處理,比方對匯流排的資料儲存,那還是工作站回收好。
設定垃圾回收方式,在開發時,可以在xxx.csproj
檔案中加入:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
其中,設定true
就是伺服器模式,設定false
就是工作站模式,當然,去掉這一行,預設也是工作站模式。
對於生產環境中已經上線的應用,也可以修改回收模式。找到程式目錄中的xxx.runtimeconfig.json
檔案,在裡面加入:
"configProperties": {
"System.GC.Server": true
}
這兩個配置的關係是:如果開發時在.csproj
中加入了ServerGarbageCollection
,那在釋出時會自動在.runtimeconfig.json
中加入System.GC.Server
。
二、回收和管理非託管資源
上面說到的回收機制,針對的是託管資源。
對於非託管資源,GC不會主動進行回收。回收非託管資源,只能手工編寫程式碼並顯式的釋放。
通常來說,程式中用到的作業系統的資原始檔、網路或資料庫連線等,都屬於非託管資源,需要手工清理。
有兩種方法可以清理非託管理資源:
- 使用終結器Finalize,並由GC回收
- 手動處理Dispose
2.1 使用終結器Finalize
終結器Finalize是System.Object
的一個虛方法,這個方法在GC回收物件的記憶體之前由垃圾回帳呼叫。我們可以重寫這個方法,來釋放非託管資源。
多說兩句:似乎MS對這個部分有些猶豫,所以這兒規則一直處在兩可之間。C#在解構函式的支援上並不嚴格。System.Object
支援重寫Object.Finalize
方法,但它建立的類卻不支援,重寫會報錯,而只能通過改寫解構函式來實現,並由編譯器將程式碼包裝在try
塊中的解構函式或重寫的Finalize
中,並由finally
呼叫Object.Finalize
來實現。
使用終結器,缺點也是比較明顯的。GC檢測到一個物件需要回收時,會在一段不確定的時間之後呼叫終結器。這個不確定很討厭,我們很難預料什麼時候物件被實際釋放。
Finalize雖然看著是手動清除非託管資源,其實還是由垃圾回收器去做的。它的最大作用是確保非託管資源一定被釋放。
2.2 手動處理Dispose
手動處理最重要的理由,是在需要的時候立即釋放,而不是讓垃圾回收器進行不確定延時後的釋放。
手動釋放,主要的工作是提供一個IDisposable.Dispose
的實現,來實現非託管資源的確定性釋放。這樣,當需要釋放時,呼叫Dispose
方法,就會立即釋放非託管資源。
手動處理實現起來很簡單。框架提供了一個介面System.IDisposable
:
public interface IDisposable
{
void Dispose();
}
他只包含一個方法Dispose
。使用時,需要實現這個方法,在使用完成後及時釋放非託管資源。
同時,Dispose
方法還提供了GC.SuppressFinalize
方法,來告訴GC物件已經被手動處理,不再需要呼叫終結器。
public void Dispose()
{
GC.SuppressFinalize(this);
}
這種方式下,物件的記憶體可以做到提前回收。
在某些情況下,可能無法呼叫IDisposable.Dispose
方法來釋放非託管資源,但場景下又確實需要確定性地釋放,這時候可能通過重寫Object.Finalize
來實現:
public class MyClass
{
~MyClass()
{
//TODO: 釋放未託管的資源
}
}
有點奇怪,是不是?
其實,這就是上邊我說MS猶豫的地方。如果你直接重寫Object.Finalize
,像下面這樣:
public class MyClass
{
protected override void Finalize()
{
//TODO: 釋放未託管的資源
}
}
編譯時會報錯Do not override object.Finalize. Instead, provide a destructor.
,而他正確的寫法,就是解構函式。
上面說的內容,做成一個套路模板,就會是這樣的:
public class MyClass : IDisposable
{
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: 釋放託管狀態(託管物件)
}
// TODO: 釋放未託管的資源(未託管的物件)並替代終結器
// TODO: 將大型欄位設定為 null
disposedValue = true;
}
}
~MyClass()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
如果你看到了這兒,建議把上面這個套路模板存下來。這算是最完整的一個版本,網上能找到的,大多是簡化版。
其實,我們經常使用的很多類,都實現了IDisposable
介面。比如說,凡是可以用using
來進行呼叫類,就都實現了IDisposable
介面。另外有一些類,把Dispose
改成了一個別的名字,比方IO裡的Close
方法,就是一個Dispose
。
另外,如果物件實現了IDisposable
介面,而我們直接new
了這個物件,那在使用結束後,我們就需要Dispose
這個物件。因為既然設計者選擇了Dispose
,那結束時呼叫Dispose
就是正確的。
三、總結
最後做個簡單的總結。
垃圾回收模式選擇:應用程式可分配的資源少,或者能夠競爭到的資源少,就使用工作站模式,反之就使用伺服器模式。
在回收處理上,託管資源就扔給GC自動處理,非託管資源需要手動處理:
其中:
- Finalize是標記出非託管資源可被回收,然後由GC去執行回收工作
- Dispose是直接呼叫,並即時回收。
(全文完)
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文版權歸作者所有,轉載請保留此宣告和原文連結 |