记录点滴收获,汇聚知识乐园

脚步无法到达的地方,目光可以到达;目光无法到达的地方,梦想可以到达

导航

内存管理、资源管理(参考整理自李建忠老师)

内存管理是一个非常重要的东西,一个好的程序员应该对内存模型和内存管理有一个好的认识。首先我们了解一下资源,资源分为两类,即托管资源和非托管资源。

托管资源:托管堆内存

非托管资源:文件句柄、数据库链接、本地内存等

这里的托管与非托管即指的是我们常说的垃圾收集器,被垃圾收集器管理的资源叫托管资源,我们常说的内存管理即指的是托管资源的管理,而资源管理即指的是非托管资源的管理。

下面我们谈谈.NET的内存管理

在分配内存的过程中,当保留的内存区域全部被用光时,这时候GC启动,通过对象图,进行扫描,找到那些不可达的对象,这些对象即为垃圾对象,然后对内存区域进行压缩,使垃圾对象被覆盖掉(这里基于一个前提:托管堆上的内存是连续分配的),由于对象的位置发生了偏移,因此需要进行指针更新。

.NET线式分配,间歇性压缩和搬移对象内存,对象地址不稳定(快、无碎片)

C++链式分配,对象地址稳定不变(慢,有碎片)

垃圾收集器的启动是不定时的,只有当内存不够使用时才会被启动,由于无内存碎片,因此C#比较适合开发服务器端应用程序。

标记压缩法的三大特点:

1.分配速度快,回收速度慢

2.确保空闲内存区域连续,避免内存碎片

3.对象地址不稳定

分代式垃圾收集:

CLR执行一个单独的线程来负责垃圾收集器,这时它会挂起当前的所有线程

分代式垃圾收集器区分代龄基于以下假设:

对象越新,其生存周期越短

对象越老,其生存周期越长

每一个托管堆对象分配一个代龄:

0代对象限额:256K

1代对象限额:2M

2代对象限额:10M

每个对象的代龄存储在对象的第一个保留字段区域

首先垃圾收集器将所有的对象分为0、1、2三代,其中大部分对象被分为0代,当0代对象越来越多,达到了内存限额256K,这时垃圾收集器启动0代收集,将那些不可达对象进行收集,那些经过0代收集仍然存活的对象,垃圾收集器会对其进行搬移,将其转为1代对象。1代和2代得收集原理和0代相似,只不过在启动1代收集时同时会启动0代收集,在启动2代收集时同时会启动0代、1代收集。

对于一些比较小的软件,垃圾收集器的二代是不会启动的,如果运行的时间非常长,二代才可能会启动,垃圾收集器的这种机制就是使那些难以回收的对象的晚一些回收,对于一些比较复杂的对象,垃圾收集器一开始就将其标记为2代对象,对于一些比较简单的对象,垃圾收集器一开始就将其标记为0代对象,这就是所谓的垃圾收集器“欺软怕硬”的精神。

代龄的内存分配是动态调整的,如果垃圾收集器发现系统频繁的调用0代、1代、2代收集器,同时它又发现你系统的内存比较大,它会把每一代的内存限额相应的调大一些,使得垃圾收集器启动的不那么频繁。这是它懒惰的表现

垃圾收集器没有提供任何一种方法只回收一个特定的对象,垃圾收集器从来不屑与去收集一个对象,因为收集一个对象的代价过高,如果我们想回收特定的某一代的对象,GC提供了一个函数

GC.Collect(),这个函数在System命名空间,其参数只能为0、1、2,如果不带参数,则强行对所有代进行垃圾回收,如果带参数,则强行对0代到指定代进行垃圾回收。这种方法是不被鼓励使用的,GetGeneration(Object),返回指定对象的当前代数。这些方法只限于实验性的使用。

资源管理(非托管资源)

GC主要负责回收内存,对于非托管资源只能进行辅助性的回收,下面我们举一个例子

using System;
class MyClass
{
int x; //纯内存对象,垃圾收集器可完全回收
int y;

IntPtr handle //资源对象,代表一个资源
public MyClass()
{
handle=OpenHandle(); //获取资源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
//............
//............
//GC启动
}
}

获取资源:即向Windows操作系统去请求资源,Windows操作系统会标记出当前进程所占用的资源,操作系统是以进程为单位来划分资源的,只要进程获取了资源,且进程不关闭,Windows操作系统就会一直认为这个资源被占用着,直到进程被关闭。

上面的代码中,我们定义了一个资源类(如数据访问类就是一个资源类),然后在测试函数中我们创建了一个资源类的对象,同时在其构造函数中获取了相应的资源,

下面我们通过图解说明:

当这个对象使用完以后就成为了垃圾对象,GC启动对对象进行回收,但是GC并不会对对象所使用的资源进行释放,因为你没有告诉垃圾收集器资源不用了。此时即产生了资源泄漏,直至进程关闭,其所占用的资源才会被释放。在一些小的程序中,即便资源泄漏,也不会对系统产生多大的影响,因为进程可能很快就会被关闭,但是对于一些服务器运行程序,其运行时间一般会很长,其造成的资源泄漏是不容忽视的。

下面我们对上面的代码进行改进:

using System;
class MyClass
{
int x; //纯内存对象,垃圾收集器可完全回收
int y;

IntPtr handle //资源对象,代表一个资源
public MyClass()
{
handle=OpenHandle(); //获取资源
}
~MyClass() //析构器
{
CloseHandle(handle); //释放资源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
//............
//............
//GC启动
}
}

在这里我们在析构器中对资源进行了释放,当GC启动的时候,如果析构器不存在,GC就会直接回收对象的内存,但由于有了析构器,这时GC就不会选择直接回收对象内存了,转而去调用析构器了,而且GC在对对象进行第一次回收时只会执行析构器,不会回收内存,也就是说垃圾收集器在一次执行过程中只能做一件事情。因此对象就会变为1代对象,这就是为什么会析构器会托大对象的代龄了。

为了避免对象的代龄被托大,同时为了能够及时的释放对象所占用的资源,把释放资源的任务交给垃圾收集器是不可靠的,下面我们看一个改进的例子:

using System;
class MyClass : IDisposable
{
int x; //纯内存对象,垃圾收集器可完全回收
int y;
IntPtr handle //资源对象,代表一个资源
public MyClass()
{
handle=OpenHandle(); //获取资源
}
public void Dispose()
{
CloseHandle(handle) //释放资源
GC.SuppressFinalize(this); //告诉系统就当我没有写析构器,避免托大代龄
}
~MyClass() //析构器
{
CloseHandle(handle) //释放资源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
obj.Dispose();
//............
//............
//GC启动
}
}

在这里我们通过IDisposable接口实现了Dispose()方法,即我们在对象使用完毕以后可以自行调用Dispose()方法来实现对资源的释放,不用等待析构器去释放资源,同时我们在这个函数中告诉垃圾收集器不要调用析构器,避免代龄被托大,其实Dispose()方法可以用一个普通的方法来表示,但是,通过接口来实现资源的释放,可以让别人一眼就能看出这是一个资源类。

这里又有一个问题,既然通过Dispose()方法实现了资源的自行释放,那为什么还需要写析构器了,原因就是以防程序员在编写程序的过程中忘记了调用Dispose()方法去释放资源,这时也会造成资源的泄露,因此为了绝对的安全,还是需要将析构器写上。

上面这些也就形成了资源处理的一个设计模式,这个设计模式有一个约定,即把释放资源的方法的名字叫做Dispose,同时该方法所在的类需要实现IDisposable接口,有了这个接口以后,就可以让使用类的程序员知道该类为一个资源类,当对象用完的时候一定要调用Dispose()方法。

析构器的本质是一个Finalize方法,当我们编译完上面的代码,用Ildasm工具去查看去IL代码,就会发现析构器被编译成了一个Finalize方法。它是重写了基类的一个受保护的虚方法。

但是如果程序在执行过程中抛出了异常,导致Dispose()方法没有被执行,这时虽然有析构器确保资源被释放,但是这并不是我们的本意,因此为了确保Dispose()方法被执行,我们需要写一个异常处理的语句结构,下面我们看一看代码

using System;
class MyClass : IDisposable
{
int x; //纯内存对象,垃圾收集器可完全回收
int y;
IntPtr handle //资源对象,代表一个资源
public MyClass()
{
handle=OpenHandle(); //获取资源
}
public void Dispose()
{
CloseHandle(handle) //释放资源
GC.SuppressFinalize(this); //告诉系统就当我没有写析构器,避免托大代龄
}
~MyClass() //析构器
{
CloseHandle(handle) //释放资源
}
public void Process()
{
//...........
}
}
class Test
{
public static void Main()
{
MyClass obj=null;
try
{
obj=new MyClass();
obj.Process();
}
catch(Exception e)
{
throw e;
}
finally
{
if (obj!=null)
{
obj.Dispose();
}
}
//............
//............
//GC启动
}
}

这样就确保了Dispose()方法会被执行,这种确保资源被释放的处理方法可以使用using语句来处理,using语句块可以确保对象的Dispose()方法被执行,不管是否出现异常,其实using语句的实现就是通过上面的异常处理方式实现的,因此前提是对象的类实现了IDisposable接口,内部具有Dispose()方法。上面的代码可以改为:
using System;
class MyClass : IDisposable
{
int x; //纯内存对象,垃圾收集器可完全回收
int y;
IntPtr handle //资源对象,代表一个资源
public MyClass()
{
handle=OpenHandle(); //获取资源
}
public void Dispose()
{
CloseHandle(handle) //释放资源
GC.SuppressFinalize(this); //告诉系统就当我没有写析构器,避免托大代龄
}
~MyClass() //析构器
{
CloseHandle(handle) //释放资源
}
public void Process()
{
//...........
}
}
class Test
{
public static void Main()
{
using(MyClass obj=new MyClass())
{
obj.Process();
}
//............
//............
//GC启动
}
}
这里补充一些:析构器里面一般只用于释放资源,最好不要去做其他事情,因为在有些情况下,析构器根本就不会被调用。








posted on 2012-02-23 12:17  guowenhui  阅读(2324)  评论(4编辑  收藏  举报