.NET 垃圾回收与内存泄漏

.NET 垃圾回收与内存泄漏

> 前言
相信你们必定听过,看过甚至遇到过内存泄漏。在 .NET 平台也必定知道有垃圾回收器,它可让开发人员没必要担忧内存的释放问题,由于它会自定管理内存。可是在 .NET 平台下进行编程,绝对不会发生内存泄漏的问题吗?答案是否认的,就算有了自动内存管理的垃圾回收器,也会发生内存泄漏。本文就讨论下 .NET 平台的垃圾回收器是如何工做的,进而当咱们在编写 .NET 程序时避免发生内存泄漏的问题。html


> 垃圾回收的基本概念
“垃圾”指的是事先分配过但后来再也不被使用的内存。
垃圾回收背后的一个基本观念是:“无限访问的内存”,可是历来没有无限的内存,当机器须要分配内存但不够的时候,就须要把以前再也不使用的内存——“垃圾”回收再利用。
.NET 的垃圾回收器正是这样作的:
.NET Framework 的垃圾回收器管理应用程序的内存分配和释放。每当您建立新对象时,公共语言运行时都会从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行时就会继续为新对象分配空间。 可是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。(引用 MSDN 垃圾回收程序员


> 垃圾回收器的工做场景
每当咱们建立一个对象的时候,系统会为新对象分配一块内存,若是有足够的可用内存则会直接分配;可是当内存不足的时候,此时垃圾回收器会进行一次回收操做,把再也不使用的对象释放,转化为可用的内存供新对象使用。算法

看似很简单的工做步骤,可是垃圾回收器怎么知道确保再也不使用的对象的呢?编程


> 垃圾回收算法
当进行一次垃圾回收操做时,会分三个步骤进行:
1. 先假设全部对象都是垃圾;
2. 标记出正在使用的对象;
  标记依据:
  a. 被变量引用的对象,仍然在做用域中。
    好比某个类中的某个方法,方法执行了一半,若是此时发生垃圾回收,那么方法块中的变量都在做用域中,那么它们都会被标记为正在使用。
  b. 被另外一个对象引用的对象,仍在使用中。
3. 压缩:释放第二步中未标记的对象(再也不使用,即“垃圾”)并将使用中的对象转移到连续的内存块中。
  只要垃圾回收器释放了能释放的对象,它就会压缩剩余的对象,把它们都移回堆的端部,再一次造成一个连续的块。
网络

备注:
垃圾回收器为了提高性能,使用了代机制,新建的对象是新一代,较早建立的对象是老一代,最近建立的对象是第0代。为了描述垃圾回收器的基本原理,本文不深刻讨论代机制。框架

总之,有了垃圾回收器,咱们没必要本身实现代码来管理应用程序所用的对象的生存期。函数

既然有了自动内存管理功能的垃圾回收器,为何还会发生内存泄漏呢?post


> 托管与非托管
由公共语言运行库环境(而不是直接由操做系统)执行的代码称做托管代码,运行在 .NET 框架下,受 .NET 框架管理的应用或组件称做托管资源。.NET 中超过80%的资源都是托管资源,如 int, string, float, DateTime。
非托管资源是 .NET 框架以外的,最多见的一类非托管资源就是包装操做系统资源的对象,例如文件,窗口或网络链接,对于这类资源虽然垃圾回收器能够跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。因此,对于非托管资源,在应用程序中使用完以后,必须显示的释放它们。
因此,大部份内存泄漏都是非托管资源内存泄漏:没有显示的释放它们。性能


> 非托管资源内存泄漏
一个会致使内存泄漏的类:this

复制代码
public class Foo
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}
复制代码

调用 Foo 类:

复制代码
static void Main(string[] args)
{
    Foo foo = new Foo();
    foo = null;
    
    Thread.Sleep(int.MaxValue);
}
复制代码

foo 虽然设置为 null,可是 foo 中的字段 _timer 依然存活,Elapsed 事件继续执行:

此类中,_timer 对象就是非托管对象,因为 _timer 的 Elapsed 事件,.NET Framework 会保持 _timer 永远存活,进而 _timer 对象会保持 Foo 实例永远存活,直到程序关闭。

为了解决这个问题,咱们要显示的释放 _timer 对象:Foo 类继承 IDisposable 接口,修改后的类:

复制代码
public class Foo : IDisposable
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
        _timer.Dispose();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}
复制代码

 

再次调用 Foo 类,并显示调用 Dispose 方法: 

复制代码
static void Main(string[] args)
{
    Foo foo = new Foo();
    foo.Dispose();
    foo = null;
    
    Thread.Sleep(int.MaxValue);
}
复制代码

foo 设置为 null,_timer 对象也同时被回收,Elapsed 事件中止:


> 非托管资源的垃圾回收
1. 析构函数。
2. 实现 IDisposable 接口。
  在咱们编写代码时,一个简单的方法就是查看类中定义的字段是否有继承 IDisposable 接口的,若是有,那么当前的类也应继承 IDisposable 接口。在使用完非托管资源时,要及时调用 Dispose 方法释放资源:

Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  
label.Dispose();

更好的方式是使用 using,using 会在编译代码的时候自动建立 try/finally 语句块,在 finally 语句块中自动调用 Dispose 方法。

using (Label label = new Label())
{
    this.Controls.Add(label);
    this.Controls.Remove(label);
}

 

> 避免内存泄漏的几点建议
除了刚刚提到的非托管资源,还有几点须要注意:
1. 订阅事件,再也不使用时要记得取消订阅。
2. 不要大量使用静态字段,静态字段会永远存活,一个静态的集合很容易引发内存溢出。

做者: 孜孜不倦的程序员 
出处: http://sirkevin.cnblogs.com/  本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。
相关文章
相关标签/搜索