C#内存管理入门

  一直以来对内存泄漏、内存溢出不太关心,而且认为 C# 的垃圾回收机制已经很完善了,所以在写代码时对之并不关心。后来被问及相关知识,就嘎屁了。
  C# 语言的一个好处是不需要考虑对内存的分配和释放。我们只需要创建对象,然后通过垃圾回收机制(GC)来处理这些对象,即:应用程序会自动清理当不再使用的对象。

  • 垃圾收集器的工作就是寻找那些不再被应用程序需要的对象,当它们不会再被访问或引用的时候清除它们。(一定要注意是在不会再被访问或引用的时候才清除它们)
  • 一个对象只有当它不再被引用的时候才会被当作是无用的

内存溢出

    指程序在运行过程中,程序对内存的需求超过计算机分配给程序的内存,从而造成“out of memory”之类的错误,使程序崩溃死亡

内存泄漏

    指由于疏忽或错误造成程序不能及时释放不再使用的内存,是应用程序分配某段内存后,由于某种原因而失去对该段内存的控制

托管资源

    托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收

非托管资源

    非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。

对象创建的过程

A a = new A();

创建A的对象并对其进行初始化。
  A:类;
  new A():创建A的对象并对其初始化;
  a:引用,指向new A()这个对象的引用。
  注意:a不是A的对象,new A()才是A的对象。

  1. 为字段分配内存并进行初始化;
  2. 如果类是第一次加载(即系统中从未创建类的其它对象,或者曾经创建但因不再有引用而被 GC 全部回收),则 Copy 其实例方法至方法表;
  3. 为类中需要直接赋值的字段赋值;
  4. 执行构造函数。

栈和托管堆

  • 对象的分配是在托管堆中执行的,
  • 栈只是存储对对象的引用。
  • 当栈在执行过程中,如果释放了对对象的引用,垃圾回收会开始回收引用计数为0的对象占用的内存。
        托管堆是以应用程序域为依托的,即每一个应用程序域有一个托管堆,每一个托管堆也只属于一个应用程序域,且托管堆是一块连续的内存,其中的对象也是紧密排列的。相对于C++中的非连续内存堆来说,托管堆的内存分配效率要高。托管堆维护了一个指针,指向当前已使用内存的末尾,当需要分配内存的时候,只需要指针向后移动指定数量的位置即可。而且托管堆通过应用程序域实现了应用程序之间内存的隔离,即不同的应用程序域之间在正常情况下是不能相互访问各自的托管堆的。

##

解决方案

    .net自带内存回收机制。说的更直接一些,就是我们使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存。而在该对象离开他的生存期后自然就被释放了。我们并没有参与其中的操作。但是往往这样,还是不够。因为.NET的GC机制有这样两个问题:
首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
其次,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
因此我们还可以了解以下方式。

1、引用计数

  引用计数是指,针对每一个对象,保存一个对该对象的引用计数,该对象的引用增加,则相应的引用计数增加。如果对象的引用计数为零,则回收该对象。

    优点:引用计数最大的优点就是容易实现。二是成本小,基本上引用计数为0的时候垃圾会被立即回收,而其他方法难以预测对象的生命周期,垃圾存在的时间都会比这个方法高。另,这种垃圾回收方式产生的中断时间最短。

    缺点:最著名的缺点就是如果对象中存在循环引用,就无法被回收。例如,下面三个对象互相引用,但是不存在从根(Root)指向的引用,所以已经是垃圾了。但是引用计数不为0.

这里写图片描述

2、标记清除法(mark-weep)

    C#中采用的是标记法回收内存,全部对象都要标记,并且只标记一次就不再标记。判断一个对象是不是垃圾取决于是否有引用,而是取决是是否被root引用。

3、手动进行垃圾回收(GC(Garbagecollection)

如上文所说,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。或使用using语句可以简化资源管理。

##

  1.在声明对象和定义变量的时候最好是在定义的时候 给一个NUll,如果 到本行代码以后不会再使用这个对象了, 请把它设置为Null
private DataTable _dt_two = new DataTable(); 
private DataTable _dt_officeInfo = new DataTable();  

  这样做很明显是不合理的,因为你也不知道在使用的过程 中到低会不会加载数据,当然这样确定 不会再出现未对对象引用到对象实例这样的错误 了,当然也说明了一个问题,这样做是不合理的,正确的应该是这样

// 全局声明
private DataTable _dt_two = null;
private DataTable _dt_officeInfo = null;

// 使用时实例化
_dt_two =getDataTable();
_dt_officeInfo =getDataTable();

// 不再使用数据
_dt_two =null;//
_dt_officeInfo =null

GC.Collect();
GC.WaitForPendingFinalizers();

  而当使用的时候 再给其真正的值。
  其实比较常见的有一点:
  A的方法挂了B的事件, 而在A需要被释放的时候却没有摘掉事件。
  那么A就不会被回收。
  因为A在挂B的时间的时候 ,在B里面保存了一个A的引用。
  其他的情况下很少有不能回收的。
  不能回收, 说明这东西还有用。 不是垃圾。

##

  2.静态变量、集合

    对于集合,我们在使用完成之后,需要即时的clear,尤其是将一些方法中的临时变量添加到集合当中之后,会导致集合膨胀,并使得其中的内存泄露。
    对于静态字段,我们应该尽量减少其可见的域,因为静态字段在整个程序运行期间都不会被释放,减少其可见域就减少了其内存泄露的可能性,注意,不到万不得以,千万不要声明静态的集合,就是使用了,那也一定要小心再小心。静态集合很容易造成内存泄露。

##

  4.非托管资源回收机制 例子

    析构函数是一种用于实现销毁类实例所操作的成员的函数。析构函数不能带参数,也不能包含访问修饰符。另外,析构函数是自动调用有的,不能显示调用。由于GC相对来说已经很完善,所以只在某些特殊的高级开发中才需要声明的。
    如果声明析构函数,开发人员可通过System.GC的静态方法在一定程度上控制垃圾回收行为。该类可用于请求执行一次回收操作,并自动用于请求是否该执行析构函数

 A a=null;
try
{
    a=new A();
}
finally
{
    if(a!=null)
    {
        a.Dispose();
    }
}

    即使处理过程中出现异常,可以确保在a上调用 Dispose();方法,总是释放a使用的任意资源

##

Public class BaseResource:IDisposable
{
    PrivateIntPtr handle; // 句柄,属于非托管资源
    PrivateComponet comp; // 组件,托管资源
    Privateboo isDisposed = false; // 是否已释放资源的标志

    PublicBaseResource
    {
    }

    //实现接口方法
    //由类的使用者,在外部显示调用,释放类资源
    Publicvoid Dispose()
    {
        Dispose(true);// 释放托管和非托管资源

        //将对象从垃圾回收器链表中移除,
        // 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数
        GC.SuppressFinalize(this);
    }

    //由垃圾回收器调用,释放非托管资源
    ~BaseResource()
    {
        Dispose(false);// 释放非托管资源
    }

    //参数为true表示释放所有资源,只能由使用者调用
    //参数为false表示释放非托管资源,只能由垃圾回收器自动调用
    //如果子类有自己的非托管资源,可以重载这个函数,添加自己的非托管资源的释放
    //但是要记住,重载此函数必须保证调用基类的版本,以保证基类的资源正常释放
    Protectedvirtual void Dispose(bool disposing)
    {
        If(!this.disposed)// 如果资源未释放 这个判断主要用了防止对象被多次释放
        {
            If(disposing)
            {
                Comp.Dispose();// 释放托管资源
            }
            closeHandle(handle);// 释放非托管资源
            handle= IntPtr.Zero;
        }
        this.disposed= true; // 标识此对象已释放
    }
}

##

class A
{
    ~A(){Console.WriteLine("这是析构函数")}
}
class Test
{
    static void Main()
    {
        A a=new A();
        a=null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

##

   5.using() { / statement / }

    using语句的后面有一对圆括号,用于应用变量的声明和实例化,该语句使得变量放在随后的语句块中。另外,变量在超出作用域后,即使出现异常,也会自动调用Dispose()方法.
    对于某些类,使用Close()方法要比Dispose()方法更具有逻辑性。例如,在处理文件或数据库时就是这样。在这些情况下,常常实现IDispose()接口,在实现一个独立的Close()方法,Close()方法只调用Dispose()方法,还可以避免额外的代码缩进。

参考

http://blog.csdn.net/fhzh520/article/details/7872305
http://www.cnblogs.com/sufei/archive/2010/01/18/1650257.html
http://blog.jobbole.com/89064/
《C#程序设计及应用教程》 V3 马骏
《C#高级教程》 v7

文章目录
  1. 1. 内存溢出
  2. 2. 内存泄漏
  3. 3. 托管资源
  4. 4. 非托管资源
  5. 5. 对象创建的过程
  6. 6. 栈和托管堆
  7. 7. 解决方案
  8. 8. 1、引用计数
  9. 9. 2、标记清除法(mark-weep)
  10. 10. 3、手动进行垃圾回收(GC(Garbagecollection)
    1. 10.0.1.   1.在声明对象和定义变量的时候最好是在定义的时候 给一个NUll,如果 到本行代码以后不会再使用这个对象了, 请把它设置为Null
    2. 10.0.2.   2.静态变量、集合
    3. 10.0.3.   4.非托管资源回收机制 例子
    4. 10.0.4.    5.using() { / statement / }
  • 11. 参考
  • |