(精华)2020年8月31日 .NET 内存溢出

162 阅读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);     
     }     
 }    
//public TestListener(TestClassHasEvent inject)
//{
//    SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
//}

//void SystemEvents_DisplaySettingsChanged(object sender, EventArgs 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 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;  
       }  
}  

注意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)));    
    }    
}  

Form2里面有个函数:

public partial class Form2 : Form     
 {     
     public Form2()     
     {     
         InitializeComponent();     
     }     
     public void frm_PropertyChanged(object sender, PropertyChangedEventArgs e)     
     {     
      
     }    
 }  

所以这种情况下,你的Event handler没有手动注销,那就肯定内存泄露了。

二:非托管资源

public partial class MemoryLeak
{
     
    public static void Demo2()
    {

        while (true)
        {
            var x = File.OpenRead($"c:\\audio.log");
        }
    }
}

三:线程等待

public partial class MemoryLeak
{
    /// <summary>
    /// https://docs.microsoft.com/zh-cn/previous-versions/dotnet/articles/ms973858(v=msdn.10)?redirectedfrom=MSDN
    /// </summary>

    public static void Demo3()
    {

        {
            while (true)
            { 
                Console.ReadLine();
                Thread t = new Thread(new ThreadStart(ThreadProc));
                t.Start();
            }
        }
    }

    static void ThreadProc()
    { 
        Thread.CurrentThread.Join();
    }
}

四:字符串

本地程序集用const,跨程序集用readnoly

public partial class MemoryLeak
    { 

        public static void Demo4()
        {
            List<string> asd = new List<string>();
            while (true)
            {
                ///String它存放的并不是普通的堆
                ///.NET有一个叫做内存驻地的东西---->字符串池--->字符串驻留池
                ///你的这些String会存放起来并不会"完全"遵照GC走
                asd.Add(Guid.NewGuid().ToString());
                if (asd.Count > 10000)
                {
                    asd.Clear();
                    Console.WriteLine("asdasdasdasd" + 1);
                    Console.WriteLine("Error info");
                    Console.WriteLine("Error  info");
                    Console.WriteLine("error");
                    Console.WriteLine("erorr");
                }
            }
        }
    }