(1).NET 應用程序中的內存程序員
您大概已經知道,.NET 應用程序中要使用多種類型的內存,包括:堆棧、非託管堆和託管堆。這裏咱們須要簡單回顧一下。正則表達式
以運行庫爲目標的代碼稱爲託管代碼,而不以運行庫爲目標的代碼稱爲非託管代碼。算法
在運行庫的控制下執行的代碼稱做託管代碼。相反,在運行庫以外運行的代碼稱做非託管代碼。COM 組件、ActiveX 接口和 Win32 API 函數都是非託管代碼的示例。數據庫
COM/COM++組件,ActiveX控件,API函數,指針運算,自制的資源文件...這些的非託管的,其它就是託管的.在CLR上編譯運行的代碼就是託管代碼。 非CLR編譯運行的代碼就是非託管代碼 。非託管代碼用dispose free using 釋放 。即便在擁有GC的託管堆上,也有可能發生內存泄漏!編程
堆棧 堆棧用於存儲應用程序執行過程當中的局部變量、方法參數、返回值和其餘臨時值。堆棧按照每一個線程進行分配,並做爲每一個線程完成其工做的一個暫存區。垃圾收集器並不負責清理堆棧,由於爲方法調用預留的堆棧會在方法返回時被自動清理。可是請注意,垃圾收集器知道在堆棧上存儲的對象的引用。當對象在一種方法中被實例化時,該對象的引用(32 位或 64 位整型值,取決於平臺類型)將保留在堆棧中,而對象自身卻存儲於託管堆中,並在變量超出範圍時被垃圾收集器收集。windows
非託管堆 非託管堆用於運行時數據結構、方法表、Microsoft 中間語言 (MSIL)、JITed 代碼等。非託管代碼根據對象的實例化方式將其分配在非託管堆或堆棧上。託管代碼可經過調用非託管的 Win32® API 或實例化 COM 對象來直接分配非託管堆內存。CLR 出於自身的數據結構和代碼緣由普遍地使用非託管堆。數組
託管堆 託管堆是用於分配託管對象的區域,同時也是垃圾收集器的域。CLR 使用分代壓縮垃圾收集器。垃圾收集器之因此稱爲分代式,是因爲它將垃圾收集後保留下來的對象按生存時間進行劃分,這樣作有助於提升性能。全部版本的 .NET Framework 都採用三代分代方法:第 0 代、第 1 代和第 2 代(從年輕代到年老代)。垃圾收集器之因此稱爲壓縮式,是由於它將對象從新定位於託管堆上,從而可以消除漏洞並保持可用內存的連續性。移動大型對象的開銷很高,所以垃圾收集器將這些大型對象分配在獨立的且不會壓縮的大型對象堆上。有關託管堆和垃圾收集器的詳細信息,請參閱 Jeffrey Richter 所著的分爲兩部分的系列文章「垃圾收集器:Microsoft .NET Framework 中的自動內存管理」和「垃圾收集器 - 第 2 部分:Microsoft .NET Framework 中的自動內存管理」。雖然該文的寫做是基於 .NET Framework 1.0,並且 .NET 垃圾收集器已經有所改進,可是其中的核心思想與 1.1 版或 2.0 版是保持一致的。緩存
可能不少.NET的用戶(甚至包括一些dot Net開發者)對Net的內存泄露不是很瞭解,甚至會說.Net不存在內存泄露,由於「不是有GC機制嗎?----」恩,是有這麼回事,它可讓你在一般應用中不用考慮使人頭疼的資源釋放問題,但很遺憾的是這個機制不保證你開發的程序就不存在內存泄露。甚至能夠說,dot Net中內存泄露是很常見的。這是由於: 一方面,GC機制自己的缺陷形成的;另外一方面,Net中託管資源和非託管資源的處理是有差別的,託管資源的處理是由GC自動執行的(執行時機是不可預知的),而非託管資源 (佔少部分,好比文件操做,網絡鏈接等)必須顯式地釋放,不然就可能形成泄露。綜合起來講的話,因爲託管資源在Net中佔大多數,一般不作顯式的資源釋放是能夠的,不會形成明顯的資源泄露,而非託管資源則否則,是發生問題的主戰場,是最須要注意的地方。 另外,不少狀況下,衰老測試主要關注的是有沒有內存泄露的發生,而對其餘泄露的重視次之。這是由於,內存跟其餘資源是正相關的,也就是說沒有內存泄露的發生,其餘泄露的發生機率也較小,其根本緣由在於幾乎全部的資源最後都會在內存上有所反應。服務器
一提到託管代碼中出現內存泄漏,不少開發人員的第一反應都認爲這是不可能的。畢竟垃圾收集器 (GC) 會負責管理全部的內存,沒錯吧?但要知道,垃圾收集器只處理託管內存。基於 Microsoft® .NET Framework 的應用程序中大量使用了非託管內存,這些非託管內存既能夠被公共語言運行庫 (CLR) 使用,也能夠在與非託管代碼進行互操做時被程序員顯式使用。在某些狀況下,垃圾管理器彷佛在逃避本身的職責,沒有對託管內存進行有效處理。這一般是因爲不易察覺的(也多是很是明顯的)編程錯誤妨礙了垃圾收集器的正常工做而形成的。做爲常常與內存打交道的程序員,咱們仍須要檢查本身的應用程序,確保它們不會發生內存泄漏並可以合理有效地使用所需內存。網絡
2 內存泄漏的種類及緣由
(1)堆棧內存泄漏
雖然有可能出現堆棧空間不足而致使在受託管的狀況下引起 StackOverflowException 異常,可是方法調用期間使用的任何堆棧空間都會在該方法返回後被回收。所以,實際上只有在兩種狀況下才會發生堆棧空間泄漏。一種狀況是進行一種極其耗費堆棧資源而且從不返回的方法調用,從而使關聯的堆棧幀沒法獲得釋放。另外一種狀況是發生線程泄漏,從而使線程的整個堆棧發生泄漏。若是應用程序爲了執行後臺工做而建立了工做線程,但卻忽略了正常終止這些進程,則可引發線程泄漏。默認狀況下,最新桌面機和服務器版的 Windows® 堆棧大小均爲 1MB。所以若是應用程序的 Process/Private Bytes 按期增大 1MB,同時 .NET CLR LocksAndThreads/# of current logical Threads 也相應增大,那麼罪魁禍首極可能是線程堆棧泄漏。下圖 顯示了(惡意的)多線程邏輯致使的不正確的線程清理示例。
Figure 清理錯誤線程
using System;
using System.Threading;
namespace MsdnMag.ThreadForker {
class Program {
static void Main() {
while(true) {
Console.WriteLine(
"Press <ENTER> to fork another thread...");
Console.ReadLine();
Thread t = new Thread(new ThreadStart(ThreadProc));
t.Start();
}
}
static void ThreadProc() {
Console.WriteLine("Thread #{0} started...",
Thread.CurrentThread.ManagedThreadId);
// Block until current thread terminates - i.e. wait forever
Thread.CurrentThread.Join();
}
}
}
當一個線程啓動後會顯示其線程 ID,而後嘗試自聯接。聯接會致使調用線程中止等待另外一線程的終止。這樣該線程就會陷入一個相似於先有雞仍是先有蛋的尷尬局面之中 — 線程要等待自身的終止。在任務管理器下查看該程序,會發現每次按 <Enter> 時,其內存使用率會增加 1MB(即線程堆棧的大小)。
每次通過循環時,Thread 對象的引用都會被刪除,但垃圾收集器並未回收分配給線程堆棧的內存。託管線程的生存期並不依賴於建立它的 Thread 對象。若是您只是由於丟失了全部與 Thread 對象相關聯的引用而不但願垃圾收集器將一個仍在運行的進程終止,這種不依賴性是很是有好處的。因而可知,垃圾收集器只是收集 Thread 對象,而非實際託管的線程。只有在其 ThreadProc 返回後或者自身被直接終止的狀況下,託管線程纔會退出(其線程堆棧的內存不會釋放)。所以,若是託管線程的終止方式不正確,分配至其線程堆棧的內存就會發生泄漏。
(2)非託管堆內存泄漏
若是總的內存使用率增長,而邏輯線程計數和託管堆內存並未增長,則代表非託管堆出現內存泄漏。咱們將對致使非託管堆中出現內存泄漏的一些常見緣由進行分析,其中包括與非託管代碼進行互操做、終結器被終止以及程序集泄漏。
與非託管代碼進行互操做:這是內存泄漏的原由之一,涉及到與非託管代碼的互操做,例如在 COM Interop 中經過 P/Invoke 和 COM 對象使用 C 樣式的 DLL。垃圾收集器沒法識別非託管內存,而正是在託管代碼的編寫過程當中錯誤地使用了非託管內存,才致使內存出現泄漏。若是應用程序與非託管代碼進行互操做,要逐步查看代碼並檢查非託管調用先後內存的使用狀況,以驗證內存是否被正確回收。若是內存未被正確回收,則使用傳統的調試方法在非託管組件中查找泄漏。
終結器被終止:當一個對象的終結器未被調用,而且其中含有用於清理對象所分配的非託管內存的代碼時,會形成隱性泄漏。在正常狀況下,終結器都將被調用,可是 CLR 不會對此提供任何保證。雖然將來可能會有所變化,可是目前的 CLR 版本僅使用一個終結器線程。請考慮這樣一種狀況,運行不正常的終結器試圖將信息記錄到脫機的數據庫。若是該運行不正常的終結器反覆嘗試對數據庫進行錯誤的訪問而從不返回,則「運行正常」的終結器將永遠沒有機會運行。該問題會不時出現,由於這取決於終結器在終結隊列中的位置以及其餘終結器採起何種行爲。
當 AppDomain 拆開時,CLR 將經過運行全部終結器來嘗試清理終結器隊列。被延遲的終結器可阻止 CLR 完成 AppDomain 拆開。爲此,CLR 在該進程上作了超時操做,隨後將中止該終止進程。可是這並不意味着世界末日已經來臨。由於一般狀況下,大多數應用程序只有一個 AppDomain,而只有進程被關閉纔會致使 AppDomain 的拆開。當操做系統進程被關閉,操做系統會對該進程資源進行恢復。但不幸的是,在諸如 ASP.NET 或 SQL Server™ 之類的宿主狀況下,AppDomain 的拆開並不意味着宿主進程的結束。另外一個 AppDomain 會在同一進程中啓動。任何因自身終結器未運行而被組件泄漏的非託管內存都將繼續保持未引用狀態,沒法被訪問,而且佔用必定空間。由於內存的泄漏會隨着時間的推移愈來愈嚴重,因此這將帶來災難性的後果。
在 .NET 1.x中,惟一的解決方法是結束並從新啓動該進程。.NET Framework 2.0 中引入了關鍵的終結器,指明在 AppDomain 關閉期間,終結器將清理非託管資源並必須得到運行的機會。有關詳細信息,請參閱 Stephen Toub 的文章:「利用 .NET Framework 的可靠性功能確保代碼穩定運行」。
程序集泄漏:程序集泄漏相對來講要常見一些。一旦程序集被加載,它只有在 AppDomain 被卸載的狀況下才能被卸載。程序集泄漏也正是由此引起的。大多數狀況下,除非程序集是被動態生成並加載的,不然這根本不算個問題。下面咱們就來看一看動態代碼生成形成的泄漏,特別要詳細分析 XmlSerializer 的泄漏。
動態代碼生成有時會泄漏咱們須要動態生成代碼。也許應用程序具備與 Microsoft Office 類似的宏腳本編寫接口來提升其擴展性。也許某個債券訂價引擎須要動態加載訂價規則,以便最終用戶可以建立本身的債券類型。也許應用程序是用於 Python 的動態語言運行庫/編譯器。在不少狀況下,出於性能方面的考慮,最好是經過編寫宏、訂價規則或 MSLI 代碼來解決問題。您可使用 System.CodeDom 來動態生成 MSLI。
下圖 中的代碼可在內存中動態生成一個程序集。該程序集可被重複調用而不會出現問題。遺憾的是,一旦宏、訂價規則或代碼有所改變,就必須從新生成新的動態程序集。原有的程序集將再也不使用,可是卻沒法從內存中清除,加載有程序集的 AppDomain 也沒法被卸載。其代碼、JITed 方法和其餘運行時數據結構所用的非託管堆內存已經被泄漏。(託管內存也在動態生成的類上以任意靜態字段的形式被泄漏。)要檢測到這一問題,咱們尚無良方妙計。若是您正使用 System.CodeDom 動態地生成 MSLI,請檢查是否從新生成了代碼。若是有代碼生成,那麼您的非託管堆內存正在發生泄漏。
CodeCompileUnit program = new CodeCompileUnit();
CodeNamespace ns = new
CodeNamespace("MsdnMag.MemoryLeaks.CodeGen.CodeDomGenerated");
ns.Imports.Add(new CodeNamespaceImport("System"));
program.Namespaces.Add(ns);
CodeTypeDeclaration class1 = new CodeTypeDeclaration("CodeDomHello");
ns.Types.Add(class1);
CodeEntryPointMethod start = new CodeEntryPointMethod();
start.ReturnType = new CodeTypeReference(typeof(void));
CodeMethodInvokeExpression cs1 = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression("System.Console"), "WriteLine",
new CodePrimitiveExpression("Hello, World!"));
start.Statements.Add(cs1);
class1.Members.Add(start);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromDom(
new CompilerParameters(), program);
目前有兩種主要方法可解決這一問題。第一種方法是將動態生成的 MSLI 加載到子 AppDomain 中。子 AppDomain 可以在所生成的代碼發生改變時被卸載,並運行一個新的子 AppDomain 來託管更新後的 MSLI。這種方法在全部版本的 .NET Framework 中都是行之有效的。
.NET Framework 2.0 中還引入了另一種叫作輕量級代碼生成的方法,也稱動態方法。使用 DynamicMethod 能夠顯式發出 MSLI 的操做碼來定義方法體,而後能夠直接經過 DynamicMethod.Invoke 或經過合適的委託來調用 DynamicMethod。
DynamicMethod dm = new DynamicMethod("tempMethod" +
Guid.NewGuid().ToString(), null, null, this.GetType());
ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello, World!");
MethodInfo cw = typeof(Console).GetMethod("WriteLine",
new Type[] { typeof(string) });
il.Emit(OpCodes.Call, cw);
dm.Invoke(null, null);
動態方法的主要優點是 MSLI 和全部相關代碼生成數據結構均被分配在託管堆上。這意味着一旦 DynamicMethod 的最後一個引用超出範圍,垃圾收集器就可以回收內存。
XmlSerializer 泄漏:.NET Framework 中的某些部分(例如 XmlSerializer)會在內部使用動態代碼生成。請看下列典型的 XmlSerializer 代碼:
XmlSerializer serializer = new XmlSerializer(typeof(Person));
serializer.Serialize(outputStream, person);
XmlSerializer 構造函數將使用反射來分析 Person 類,並藉今生成一對由 XmlSerializationReader 和 XmlSerializationWriter 派生而來的類。它將建立臨時的 C# 文件,將結果文件編譯成臨時程序集,並最終將該程序集加載到進程。經過這種方式生成的代碼一樣須要至關大的開銷。所以 XmlSerializer 對每種類型的臨時程序集進行緩存。也就是說,下一次爲 Person 類建立 XmlSerializer 時,會使用緩存的程序集,而再也不生成新的程序集。
默認狀況下,XmlSerializer 所使用的 XmlElement 名稱就是該類的名稱。所以,Person 將被序列化爲:
<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Id>5d49c002-089d-4445-ac4a-acb8519e62c9</Id>
<FirstName>John</FirstName>
<LastName>Doe</LastName>
</Person>
有時有必要在不改變類名稱的前提下改變根元素的名稱。(要與現有架構兼容可能須要根元素名稱。)所以 Person 可能須要被序列化爲 <PersonInstance>。XmlSerializer 構造函數可以很方便地被重載,將根元素名稱做爲第二參數,以下所示:
XmlSerializer serializer = new XmlSerializer(typeof(Person),
new XmlRootAttribute("PersonInstance"));
當應用程序開始對 Person 對象進行序列化/反序列化時,一切運轉正常,直至引起 OutOfMemoryException。對 XmlSerializer 構造函數的重載並不會對動態生成的程序集進行緩存,而是在每次實例化新的 XmlSerializer 時生成新的臨時程序集。這時應用程序以臨時程序集的形式泄漏非託管內存。
要修復該泄漏,請在類中使用 XmlRootAttribute 以更改序列化類型的根元素名稱:
[XmlRoot("PersonInstance")]
public class Person {
// code
}
若是直接將屬性賦予類型,則 XmlSerializer 對爲類型所生成的程序集進行緩存,從而避免了內存的泄漏。若是須要對根元素名稱進行動態切換,應用程序可以利用工廠對其進行檢索,從而對 XmlSerializer 實例自身進行緩存。
XmlSerializer serializer = XmlSerializerFactory.Create(
typeof(Person), "PersonInstance");
XmlSerializerFactory 是我建立的一個類,它可使用 PersonInstance 根元素名稱來檢查 Dictionary<Tkey, Tvalue> 中是否包含有用於 Person 的 Xmlserializer。若是包含,則返回該實例。若是不包含,則建立一個新的實例,並將其存儲在哈希表中返回給調用方。
(3)「泄漏」託管堆內存
如今讓咱們關注一下託管內存的「泄漏」。在處理託管內存時,垃圾收集器會幫助咱們完成絕大部分的工做。咱們須要向垃圾收集器提供工做所需的信息。可是,在不少場合下,垃圾收集器沒法有效地工做,致使須要使用比正常工做要求更高的託管內存。這些狀況包括大型對象堆碎片、沒必要要的根引用以及中年危機。
(4)大型對象堆碎片 若是一個對象的大小爲 85,000 字節或者更大,就要被分配在大型對象堆上。請注意,這裏是指對象自身的大小,並不是任何子對象的大小。如下列類爲例:
public class Foo {
private byte[] m_buffer = new byte[90000]; // large object heap
}
因爲 Foo 實例僅含有一個 4 字節(32 位框架)或 8 字節(64 位框架)的緩衝區引用,以及一些 .NET Framework 使用的內務數據,所以將被分配在普通的分代式託管堆上。緩衝區將分配在大型對象堆上。
與其餘的託管堆不一樣,因爲移動大型對象耗費資源,因此大型對象堆不會被壓縮。所以,當大型對象被分配、釋放並清理後,就會出現空隙。根據使用模式的不一樣,大型對象堆中的這些空隙可能會使內存使用率明顯高於當前分配的大型對象所需的內存使用率。本月下載中包含的 LOHFragmentation 應用程序會在大型對象堆中隨機分配和釋放字節數組,從而用實例證明了這一點。應用程序運行幾回後,能經過釋放字節數組的方式建立出剛好與空隙相符的新的字節數組。在應用程序的另外幾回運行中,則未出現這種狀況,內存須要量遠遠大於當前分配的字節數組的內存須要量。您可使用諸如 CLRProfiler 的內存分析器來將大型對象堆的碎片可視化。下圖 中的紅色區域爲已分配的字節數組,而白色區域則表明未分配的空間。
圖 CLRProfiler 中的大型對象堆 (單擊該圖像得到較大視圖)
目前尚無一種單一的解決方案可以避免大型對象堆碎片的產生。您可使用相似 CLRProfiler 的工具對應用程序的內存使用狀況,特別是大型對象堆中的對象類型進行檢查。若是碎片是因爲從新分配緩衝區而產生的,則請保持固定數量的重用緩衝區。若是碎片是因爲大量字符串串連而產生的,請檢查 System.Text.StringBuilder 類是否可以減小建立臨時字符串的數量。基本策略是要肯定如何下降應用程序對臨時大型對象的依賴,而臨時大型對象正是大型對象堆中產生空隙的緣由所在。
(5)沒必要要的根引用 讓咱們思考一下垃圾收集器是如何決定回收內存的時間。當 CLR 試圖分配內存並保留不足的內存時,它就在扮演着垃圾收集器的角色。垃圾收集器列出了全部的根引用,包括位於任何線程的調用堆棧上的靜態字段和域內局部變量。垃圾收集器將這些引用標記爲可訪問,並跟據這些對象所包含的引用,將其一樣標記爲可訪問。這一過程將持續進行,直至全部可訪問的引用均被訪問。任何沒有被標記的對象都是沒法訪問的,所以是垃圾。垃圾收集器對託管堆進行壓縮,整理引用以指向它們在堆中的新位置,並將控件返回給 CLR。若是釋放充足的內存,則使用此釋放的內存進行分配。若是釋放的內存不足,則向操做系統請求額外的內存。
若是咱們忘記清空根引用,系統會當即阻止垃圾收集器有效地釋放內存,從而致使應用程序須要更多的內存。問題可能微妙,例如一種方法,它可以在作出與查詢數據庫或調用某個 Web 服務相相似的遠程調用前爲臨時對象建立大型圖形。若是垃圾收集發生在遠程調用期間,則整個圖形被標記爲可訪問的,並不會收集。這樣會致使更大的開銷,由於在收集中得以保留的對象將被提高到下一代,這將引發所謂的中年危機。
(6)中年危機 中年危機不會使應用程序去購買一輛保時捷。但它卻能夠形成託管堆內存的過分使用,並使垃圾收集器花費過多的處理器時間。正如前面所提到的,垃圾收集器使用分代式算法,採起試探性的推斷,它會認爲若是對象已經存活一段時期,則有可能存活更長的一段時期。例如,在 Windows 窗體應用程序中,應用程序啓動時會建立主窗體,主窗體關閉時應用程序則退出。對於垃圾收集器來講,持續地驗證主窗體是否正在被引用是一件浪費資源的事。當系統須要內存以知足分配請求時,會首先執行第 0 代收集。若是沒有足夠的可用內存,則執行第 1 代收集。若是仍然沒法知足分配請求,則繼續執行第 2 代收集,這將致使整個託管堆以極大的開銷進行清理工做。第 0 代收集的開銷相對較低,由於只有當前被分配的對象才被認爲是須要收集的。
若是對象有繼續存活至第 1 代(或更嚴重至第 2 代)的趨勢,但卻隨即死亡,此時就會出現中年危機。這樣作的效果是使得開銷低的第 0 代收集轉變爲開銷大得多的第 1 代(或第 2 代)收集。爲何會發生這種現象呢?請看下面的代碼:
class Foo {
~Foo() { }
}
對象將始終在第 1 代收集中被回收!終結器 ~Foo() 使咱們能夠實現對象的代碼清理,除非強行終止 AppDomain,不然代碼將在對象內存被釋放前運行。垃圾收集器的任務是儘快地釋放盡量多的託管內存。終結器是由用戶編寫的代碼,而且毫無疑問能夠執行任何操做。雖然咱們並不建議,可是終結器也會執行一些愚蠢的操做,例如將日誌記錄到數據庫或調用 Thread.Sleep(int.MaxValue)。所以,當垃圾收集器發現具備終結器但未被引用的對象時,會將該對象加入到終結隊列中,並繼續工做。該對象由此在垃圾收集中得以保留,被提高一代。這裏甚至爲其準備了一個性能計數器:.NET CLR Memory-Finalization Survivors,可顯示最後一次垃圾收集期間因爲具備終結器而得以保留的對象的數量。最後,終結器線程將運行對象的終結器,隨後對象即被收集。但此時您已經從開銷低的第 0 代收集轉變爲第 1 代收集,而您僅僅是添加了一個終結器!
大多數狀況下,編寫託管代碼時終結器並非必不可少的。只有當託管對象具備須要清理的非託管資源的引用時,才須要終結器。並且即便這樣,您也應該使用 SafeHandle 派生類型來對非託管資源進行包裝,而不要使用終結器。此外,若是您使用非託管資源或其餘實現 Idispoable 的託管類型,請實現 Dispose 模式來讓使用對象的用戶大膽地清理資源,並避免使用任何相關的終結器。
若是一個對象僅擁有其餘託管對象的引用,垃圾收集器將對未引用的對象進行清理。這一點與 C++ 大相徑庭,在 C++ 中必須在子對象上調用刪除命令。若是終結器爲空或僅僅將子對象引用清空,請將其刪除。將對象沒必要要地提高至更高一代將對性能形成影響,使清理開銷更高。
還有一些作法會致使中年危機,例如在進行查詢數據庫、在另外一線程上阻塞或調用 Web 服務等阻塞調用以前保持對對象的持有。在調用過程當中,能夠發生一次或屢次收集,並由此使得開銷低的第 0 代對象提高至更高一代,從而再次致使更高的內存使用率和收集成本。
還有一種狀況,它與事件處理程序和回調一塊兒發生而且更難理解。我將以 ASP.NET 爲例,但一樣類型的問題也會發生在任何應用程序中。考慮一下執行一次開銷很大的查詢,而後等上 5 分鐘才能夠緩存查詢結果的狀況。查詢是屬於頁面查詢,並基於查詢字符串參數來進行。當一項內容從緩存中刪除時,事件處理程序將進行記錄,以監視緩存行爲。(參見下圖)。
記錄從緩存中移除的項
protected void Page_Load(object sender, EventArgs e) {
string cacheKey = buildCacheKey(Request.Url, Request.QueryString);
object cachedObject = Cache.Get(cacheKey);
if(cachedObject == null) {
cachedObject = someExpensiveQuery();
Cache.Add(cacheKey, cachedObject, null,
Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(5), CacheItemPriority.Default,
new CacheItemRemovedCallback(OnCacheItemRemoved));
}
... // Continue with normal page processing
}
private void OnCacheItemRemoved(string key, object value,
CacheItemRemovedReason reason) {
... // Do some logging here
}
看上去正常的代碼實際上隱含着嚴重的錯誤。全部這些 ASP.NET Page 實例都變成了「永世長存」的對象。OnCacheItemRemoved 是一個實例方法,CacheItemRemovedCallback 委託中包含了一個隱式的「this」指針,這裏的「this」即爲 Page 實例。該委託被添加至 Cache 對象。這樣,就會產生一個從 Cache 到委託再到 Page 實例的依賴關係。在進行垃圾收集時,能夠一直從根引用(Cache 對象)訪問 Page 實例。這時,Page 實例(以及在呈現時它所建立的全部臨時對象)至少須要等待五分鐘才能被收集,在此期間,它們都有可能被提高至第 2 代。幸運地是,有一種簡單的方法可以解決該示例中的問題。請將回調函數變爲靜態。Page 實例上的依賴關係就會被打破,從而能夠像第 0 代對象同樣以很低的開銷來進行收集。
3..Net內存泄露的檢測
(1)如何檢測泄漏
不少跡象可以代表應用程序正在發生內存泄漏。或許應用程序正在引起 OutOfMemoryException。或許應用程序因啓動了虛擬內存與硬盤的交換而變得響應遲緩。或許出現任務管理器中內存的使用率逐漸(也可能忽然地)上升。當懷疑應用程序發生內存泄漏時,必須首先肯定是哪一種類型的內存發生泄漏,以便您將調試工做的重點放在合適的區域。使用 PerfMon 來檢查用於應用程序的下列性能計數器:Process/Private Bytes、.NET CLR Memory/# Bytes in All Heaps 和 .NET CLR LocksAndThreads/# of current logical Threads。Process/Private Bytes 計數器用於報告系統中專門爲某一進程分配而沒法與其餘進程共享的全部內存。.NET CLR Memory/# Bytes in All Heaps 計數器報告第 0 代、第 1 代、第 2 代和大型對象堆的合計大小。.NET CLR LocksAndThreads/# of current logical Threads 計數器報告 AppDomain 中邏輯線程的數量。若是應用程序的邏輯線程計數出現意想不到的增大,則代表線程堆棧發生泄漏。若是 Private Bytes 增大,而 # Bytes in All Heaps 保持不變,則代表非託管內存發生泄漏。若是上述兩個計數器均有所增長,則代表託管堆中的內存消耗在增加。 有沒有內存泄露的發生?判斷依據是那些?
若是程序報「Out of memory」之類的錯誤,事實上也佔據了很大部分的內存,應該說是典型的內存泄露,這種狀況屬於完全的Bug,解決之道就是找到問題點,改正。但個人經驗中,這種三下兩下的就明顯的泄露的狀況較少,除非有人在很困的狀況下編碼,不然大可能是隱性或漸進式地泄露,這種需通過較長時間的衰老測試才能發現,或者在特定條件下才出現,對這種狀況要肯定問題比較費勁,有一些工具(詳見1.3)能夠利用,但我總感受效果通常,也多是我不會使用吧,我想大型程序估計得迫不得已的用這個,詳細的參見相關手冊。
須要強調的是,判斷一個程序是否是出現了"memory leak",關鍵不是看它佔用的內存有多大,而是放在一個足夠長的時期(程序進入穩定運行狀態後)內,看內存是否是仍是一直往上漲,所以,剛開始的漲動或者前期的漲動不能作爲泄露的充分證據。
以上是些比較感性的說法,實際操做中是經過一些性能計數器來測定的。大多數時候,主要關注Process 裏的如下幾個指標就能得出結論,若是這些量總體來看是持續上升的,基本能夠判斷是有泄露狀況存在的。
A.Handle Count
B.Thread Count
C.Private Bytes
D.Virtual Bytes
E.Working Set
F.另外.NET CLR Memory下的Bytes in all heeps也是我比較關注的。
經過觀察,若是發現這些參數是在一個區間內震盪的,應該是沒有大的問題,但若是是一個持續上漲的狀態,那就得注意,極可能存在內存泄露。
(2)內存泄露診斷工具
1.1如何測定以上的性能計數器
大多使用windows自帶的perfmon.msc。
1.2其餘一些重要的性能計數器
重要的計數器
1.3其餘檢測工具
用過的工具裏面CLRProfiler 和dotTrace還行,windeg也還行。不過坦白的說,準肯定位比較費勁,最好仍是按常規的該Dispose的加Dispose,也能夠加 GC.Collect()。
4.如何防止內存泄露
(1) Dispose()的使用
若是使用的對象提供Dispose()方法,那麼當你使用完畢或在必要的地方(好比Exception)調用該方法,特別是對非託管對象,必定要加以調 用,以達到防止泄露的目的。另外不少時候程序提供對Dispose()的擴展,好比Form,在這個擴展的Dispose方法中你能夠把大對象的引用什麼 的在退出前釋放。
對於DB鏈接,COM組件(好比OLE組件)等必須調用其提供的Dispose方法,沒有的話最好本身寫一個。
(2) using的使用
using除了引用Dll的功用外,還能夠限制對象的適用範圍,當超出這個界限後對象自動釋放,好比
using語句的用途
定義一個範圍,將在此範圍以外釋放一個或多個對象。
能夠在 using 語句中聲明對象:
using (Font font1 = new Font("Arial", 10.0f))
{
// use font1
}
或者在 using 語句以前聲明對象:
Font font2 = new Font("Arial", 10.0f);
using (font2)
{
// use font2
}
能夠有多個對象與 using 語句一塊兒使用,可是必須在 using 語句內部聲明這些對象:
using (Font font3 = new Font("Arial", 10.0f),font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
(3) 事件的卸載
這個不是必須的,推薦這樣作。以前註冊了的事件,關閉畫面時應該手動註銷,有利於GC回收資源。
(4) API的調用
通常的使用API了就意味着使用了非託管資源,須要根據狀況手動釋放所佔資源,特別是在處理大對象時。 4.5繼承 IDisposable實現本身內存釋放接口 Net 如何繼承IDisposable接口,實現本身的Dispose()函數
(5)弱引用(WeakReference )
一般狀況下,一個實例若是被其餘實例引用了,那麼他就不會被GC回收,而弱引用的意思是,若是一個實例沒有被其餘實例引用(真實引用),而僅僅是被弱引 用,那麼他就會被GC回收。
(6)析構函數(Finalize())
使用了非託管資源的時候,能夠自定義析構函數使得對象結束時釋放所佔資源;
對僅使用託管資源的對象,應儘量使用它自身的Dispose方法,通常不推薦自定義析構函數。
根據廣泛意義上的內存泄漏定義,大多數的.NET內存對象在再也不被使用後都會有短暫的一段時間的內存泄漏,由於要等待下一個GC時纔有可能會被釋放。但這種狀況並不會對系統形成大的危害。
其實真正影響系統的嚴重內存泄漏狀況如:
1:大對象的分配。
根據CLR的設計,.NET中的大對象將分配在託管堆內的一個特殊的區域,在回收大對象的時候,並不會像變通區域回收完成時要作內存碎片整理,這是由於這個區域都是大對象,對大對象的移動成本太大了。所以若是原本有三個連續的大對象,如今中間這個要釋放掉了,而後新分配進來一個稍小點的大對象,這樣勢必在中間產生小的內存碎片,這個部分又沒法利用。就形成了內存泄漏,而且除非碎片相鄰的大對象被釋放掉外,無法解決。 所以在編程時要注意大對象的操做,儘可能減小大對象的分配次數。
2:避免根引用對象的分配
所謂的根引用對象就是那些GC不會去釋放的對象引用。好比類的公共靜態變量。 GC會視該變量對象在整個程序生命週期中都有效。所以就不會釋放它。當它自己比較大,或者它內部又想用了其它不少對象時,這一連串的對象都沒法在整個生命週期中獲得釋放。形成了較大的內存泄漏,應該時時注意這種風險的發生。
3:不合理的Finalize() 方法定義。
5.總結
以上已經就 .NET 應用程序中可以致使內存泄漏或內存消耗過分的各類問題進行了討論。雖然 .NET 可減小您對內存方面的關注程度,可是您仍必須關注應用程序的內存使用狀況,以確保應用程序高效正常運行。雖然應用程序被託管,但這並不意味着您能夠依靠垃圾收集器就能解決全部問題而將良好的軟件工程實踐束之高閣。雖然在應用程序的開發和測試階段,您必須對其內存性能進行持續不斷的監視。可是這樣作很是值得。要記住,只有讓用戶滿意才稱得上是功能良好的應用程序。
關於.NET有一個鮮有人言及的問題,它和使用動態代碼生成有關。簡而言之,在XML序列化、正則表達式和XLST轉換中用到的動態代碼生成功能會引發內存泄漏。
儘管公共語言運行時(Common Language Runtime,CLR)能卸載整個應用程序域(App Domain),可是它沒法卸載個別的Assemblies。代碼生成依賴於建立臨時Assemblies。一般這些Assemblies會被加載進主應用程序域中,這也就是說,不到應用程序退出時,它們都沒法被卸載。
對於諸如XML序列化的庫來講,這個問題並不大。一般,一個給定類型的序列化代碼都會緩存起來,這樣應用程序則被限制在每類型只有一個臨時Assembly。但有些XMLSerializer的重載沒有使用緩存。假如開發人員使用了它們,又沒有提供在必定程度的應用程序級別的緩存,那麼隨着本質上相同的代碼的新實例不斷被加載到內存中,內存將會慢慢發生泄漏。