一直以來都對內存泄露和內存溢出理解的不是很深刻。在網上看到了幾篇文章,於是整理了一下自己對內存泄露和內存溢出的理解。
一.概念
內存溢出:指程序在運行的過程中,程序對內存的需求超過了超過了計算機分配給程序的內存,從而造成“Out of memory”之類的錯誤,使程序不能正常運行。
造成內存溢出有幾種情況: 1.計算機本身的內存小,當同時運行多個軟件時,計算機得內存不夠用從而造成內存溢出。對於這種情況,只能增加計算機內存來解決。 2.軟件程序的問題,程序在運行時沒能及時釋放不用的內存,造成使用的內存越來越大從而造成內存溢出。對於這種情況,可以修改程序的代碼來解決。
內存泄露:內存泄漏指由於疏忽或錯誤造成程序不能釋放或不能及時釋放已經不再使用的內存的情況,是應用程序分配某段內存后,由於設計錯誤,失去了對該段內存的控制,因而造成了內存不能回收和不能及時回收。當程序不能釋放的內存越來越多是就會造成程序的性能下降或出現內存溢出的錯誤。
二、內存泄露檢測工具:
1. SciTech Software AB .NET Memory Profiler-找到內存泄漏並優化內存使用針對C#,VB.Net,或其它.Net程序。
2. YourKit .NET & Java Profiler-業界領先的Java和.NET程序性能分析工具。
3. AutomatedQA AQTime-AutomatedQA的獲獎產品performance profiling和memory debugging工具集的下一代替換產品,支持Microsoft, Borland, Intel, Compaq 和 GNU編譯器。可以為.NET和Windows程序生成全面細致的報告,從而幫助您輕松隔離並排除代碼中含有的性能問題和內存/資源泄露問題。支持.Net 1.0,1.1,2.0,3.0和Windows 32/64位應用程序。
4. JavaScript Memory Leak Detector-微軟全球產品開發歐洲團隊(Global Product Development- Europe team, GPDE) 發布的一款調試工具,用來探測JavaScript代碼中的內存泄漏,運行為IE系列的一個插件。
5.使用LoadRunner,使用方法http://www.cnblogs.com/mayingbao/archive/2007/12/20/1006818.html
6.使用 .Net Memory Profiler 工具,使用方法見:http://lzy.iteye.com/blog/344317
7.在單元測試時,在代碼中檢測,如.net 下 使用Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));代碼可以查看當前使用的內存。
二、導致內存泄露的常見情況及解決方法:
1.未退訂的事件
是否沒有手動注銷事件就會造成內存泄露,我們先看這個問題
- class TestClassHasEvent
- {
- public delegate void TestEventHandler(object sender, EventArgs e);
- public event TestEventHandler YourEvent;
- protected void OnYourEvent(EventArgs e)
- {
- if (YourEvent != null) YourEvent(this, e);
- }
- }
- class TestListener
- {
- byte[] m_ExtraMemory = new byte[1000000];
- private TestClassHasEvent _inject;
- public TestListener(TestClassHasEvent inject)
- {
- _inject = inject;
- _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent);
- }
- void _inject_YourEvent(object sender, EventArgs e)
- {
- }
- }
- class Program
- {
- static void DisplayMemory()
- {
- Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
- }
- static void Main()
- {
- DisplayMemory();
- Console.WriteLine();
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("--- New Listener #{0} ---", i + 1);
- var listener = new TestListener(new TestClassHasEvent());
- ////listener = null; //可有可無
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- DisplayMemory();
- }
- Console.Read();
- }
- }
class TestClassHasEvent { public delegate void TestEventHandler(object sender, EventArgs e); public event TestEventHandler YourEvent; protected void OnYourEvent(EventArgs e) { if (YourEvent != null) YourEvent(this, e); } } class TestListener { byte[] m_ExtraMemory = new byte[1000000]; private TestClassHasEvent _inject; public TestListener(TestClassHasEvent inject) { _inject = inject; _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent); } void _inject_YourEvent(object sender, EventArgs e) { } } class Program { static void DisplayMemory() { Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true)); } static void Main() { DisplayMemory(); Console.WriteLine(); for (int i = 0; i < 5; i++) { Console.WriteLine("--- New Listener #{0} ---", i + 1); var listener = new TestListener(new TestClassHasEvent()); ////listener = null; //可有可無 GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); DisplayMemory(); } Console.Read(); } }
運行結果:
我們來改一行代碼:
把下面這段:
- public TestListener(TestClassHasEvent inject)
- {
- _inject = inject;
- _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent);
- }
public TestListener(TestClassHasEvent inject) { _inject = inject; _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent); }
改成:
- public TestListener(TestClassHasEvent inject)
- {
- SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
- }
- void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
- {
- }
public TestListener(TestClassHasEvent inject) { SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged); } void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) { }
看看運行結果:
內存泄露了
加個Dispose手動注銷事件,然后使用Using關鍵字,就沒有問題了
- class TestListener : IDisposable
- {
- byte[] m_ExtraMemory = new byte[1000000];
- private TestClassHasEvent _inject;
- public TestListener(TestClassHasEvent inject)
- {
- SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
- }
- void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
- {
- }
- #region IDisposable Members
- public void Dispose()
- {
- SystemEvents.DisplaySettingsChanged -= new EventHandler(SystemEvents_DisplaySettingsChanged);
- }
- #endregion
- }
- class Program
- {
- static void DisplayMemory()
- {
- Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
- }
- static void Main()
- {
- DisplayMemory();
- Console.WriteLine();
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("--- New Listener #{0} ---", i + 1);
- using (var listener = new TestListener(new TestClassHasEvent()))
- {
- //do something
- }
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- DisplayMemory();
- }
- Console.Read();
- }
- }
class TestListener : IDisposable { byte[] m_ExtraMemory = new byte[1000000]; private TestClassHasEvent _inject; public TestListener(TestClassHasEvent inject) { SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged); } void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) { } #region IDisposable Members public void Dispose() { SystemEvents.DisplaySettingsChanged -= new EventHandler(SystemEvents_DisplaySettingsChanged); } #endregion } class Program { static void DisplayMemory() { Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true)); } static void Main() { DisplayMemory(); Console.WriteLine(); for (int i = 0; i < 5; i++) { Console.WriteLine("--- New Listener #{0} ---", i + 1); using (var listener = new TestListener(new TestClassHasEvent())) { //do something } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); DisplayMemory(); } Console.Read(); } }
上面兩個例子一個內存泄露,一個沒有內存泄露,我想你應該知道原因了,根本區別在於后者有個SystemEvents.DisplaySettingsChanged事件,這個事件是靜態Static事件,所以綁定到這個事件上的對象都不會被釋放
- // Type: Microsoft.Win32.SystemEvents
- // Assembly: System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- // Assembly location: C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\System.dll
- using System;
- using System.ComponentModel;
- namespace Microsoft.Win32
- {
- public sealed class SystemEvents
- {
- public static IntPtr CreateTimer(int interval);
- public static void InvokeOnEventsThread(Delegate method);
- public static void KillTimer(IntPtr timerId);
- public static event EventHandler DisplaySettingsChanging;
- public static event EventHandler DisplaySettingsChanged;
- public static event EventHandler EventsThreadShutdown;
- public static event EventHandler InstalledFontsChanged;
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("This event has been deprecated. http://go.microsoft.com/fwlink/?linkid=14202")]
- [Browsable(false)]
- public static event EventHandler LowMemory;
- public static event EventHandler PaletteChanged;
- public static event PowerModeChangedEventHandler PowerModeChanged;
- public static event SessionEndedEventHandler SessionEnded;
- public static event SessionEndingEventHandler SessionEnding;
- public static event SessionSwitchEventHandler SessionSwitch;
- public static event EventHandler TimeChanged;
- public static event TimerElapsedEventHandler TimerElapsed;
- public static event UserPreferenceChangedEventHandler UserPreferenceChanged;
- public static event UserPreferenceChangingEventHandler UserPreferenceChanging;
- }
- }
// Type: Microsoft.Win32.SystemEvents // Assembly: System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 // Assembly location: C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\System.dll using System; using System.ComponentModel; namespace Microsoft.Win32 { public sealed class SystemEvents { public static IntPtr CreateTimer(int interval); public static void InvokeOnEventsThread(Delegate method); public static void KillTimer(IntPtr timerId); public static event EventHandler DisplaySettingsChanging; public static event EventHandler DisplaySettingsChanged; public static event EventHandler EventsThreadShutdown; public static event EventHandler InstalledFontsChanged; [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This event has been deprecated. http://go.microsoft.com/fwlink/?linkid=14202")] [Browsable(false)] public static event EventHandler LowMemory; public static event EventHandler PaletteChanged; public static event PowerModeChangedEventHandler PowerModeChanged; public static event SessionEndedEventHandler SessionEnded; public static event SessionEndingEventHandler SessionEnding; public static event SessionSwitchEventHandler SessionSwitch; public static event EventHandler TimeChanged; public static event TimerElapsedEventHandler TimerElapsed; public static event UserPreferenceChangedEventHandler UserPreferenceChanged; public static event UserPreferenceChangingEventHandler UserPreferenceChanging; } }
注意Static,注意Singleton 這種static的東西生命周期很長,永遠不會被GC回收,一旦被他給引用上了,那就不可能釋放了。上面的例子就是SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);那就意味着這個類被SystemEvents.DisplaySettingsChanged 引用了,通過它的函數。另外一個要注意的是Singleton單例模式實現的類,他們也是static的生命周期很長,要注意引用鏈,你的類是否被它引用上,如果在它的引用鏈上,就內存泄露了。
另外還有注意程序運行期間不會釋放的對象的事件
還有一種情況,既不是你的對象被static對象而不能釋放,也不是Singleton,而是你的對象被一個永遠不釋放的對象引用着,這個對象或許不是static的。這種類型很多,比如你的界面有個MainForm,嘿嘿,這個MainForm永遠不會關閉和釋放的,被它引用了那就不會釋放了。看個例子:
MainForm里面有個public event,MainForm里面打開Form2,然后關閉,看看Form2能不能釋放:
- public partial class MainForm : Form
- {
- public event PropertyChangedEventHandler PropertyChanged;
- protected virtual void OnPropertyChanged(string propertyName)
- {
- PropertyChangedEventHandler handler = PropertyChanged;
- if (handler != null)
- handler(this, new PropertyChangedEventArgs(propertyName));
- }
- public MainForm()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- Form2 frm = new Form2();
- this.PropertyChanged += frm.frm_PropertyChanged;
- //MainForm referenced form2, because main form is not released, therefore form2 will not released.
- DialogResult d = frm.ShowDialog();
- GC.Collect();
- ShowTotalMemory();
- }
- private void ShowTotalMemory()
- {
- this.listBox1.Items.Add(string.Format("Memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true)));
- }
- }
public partial class MainForm : Form { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } public MainForm() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Form2 frm = new Form2(); this.PropertyChanged += frm.frm_PropertyChanged; //MainForm referenced form2, because main form is not released, therefore form2 will not released. DialogResult d = frm.ShowDialog(); GC.Collect(); ShowTotalMemory(); } private void ShowTotalMemory() { this.listBox1.Items.Add(string.Format("Memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true))); } }
Form2里面有個函數:
- public partial class Form2 : Form
- {
- public Form2()
- {
- InitializeComponent();
- }
- public void frm_PropertyChanged(object sender, PropertyChangedEventArgs e)
- {
- }
- }
public partial class Form2 : Form { public Form2() { InitializeComponent(); } public void frm_PropertyChanged(object sender, PropertyChangedEventArgs e) { } }
所以這種情況下,你的Event handler沒有手動注銷,那就肯定內存泄露了。
2.靜態變量
靜態變量中的成員所占的內存不果不手動處理是不會釋放內存的,單態模式的對象也是靜態的,所以需要特別注意。因為靜態對象中的成員所占的內存不會釋放,如果此成員是以個對象,同時此對象中的成員所占的內存也不會釋放,以此類推,如果此對象很復雜,而且是靜態的就很容易造成內存泄露。
3.非托管資源
因為非托管資源所占的內存不能自動回收,所以使用后必須手動回收,否則程序運行多次很容易造成內存泄露
4.Dispose方法沒被調用,或Dispose方法沒有處理對象的釋放。這樣也會造成內存泄露
5.當一個查詢語句查詢出來的數據量很大,達到幾百萬條數據時存放到datatable 或dataset中也會造成內存溢出,這是可以采用分頁查詢等其他方法來解決