当前位置: 首页 > 科技观察

.NET对象清理:垃圾收集和资源清理

时间:2023-03-13 21:37:06 科技观察

【.com原稿】垃圾收集和资源清理是.NET中最重要的内容,也是所有程序都必须使用的机制,但是有很大的区别一些开发者不知道垃圾回收和资源清理的原理。然后,通过这篇文章,我将向读者详细解释垃圾收集和资源清理。1.垃圾收集。.NET中的垃圾收集是运行时的核心功能。它的作用是回收不再被引用的对象占用的内存。这里要注意垃圾回收器只回收内存资源,不处理其他资源。另外,垃圾回收器是根据有没有引用来决定清理那些东西的,也就是说垃圾回收器处理的是没有被引用的引用对象,只能回收堆上的内存。简介.NET中垃圾收集的许多细节都与CLI相关。Microsoft.NET框架中用于实现垃圾回收的算法是标记压缩算法。当每个垃圾回收周期开始时,它会查找对该对象的所有根引用(通常根引用来自静态变量、CPU寄存器以及对局部变量或参数实例的任何引用)。基于所有找到的根引用,垃圾收集器可以遍历每个根引用标识的树结构,并递归确定每个根引用指向的对象,从而标识所有可达对象。进行垃圾回收时,垃圾回收器将所有可达的对象一个一个地放在一起,这样就可以覆盖不可达对象占用的内存。为了定位和移动可达对象,进程中的所有托管线程在垃圾收集期间被挂起,以便垃圾收集器在运行期间保持状态一致性。虽然这样做会导致应用程序短暂停止工作,但一般来说,只要垃圾回收周期不是特别长,这种短暂的停止工作是很难被察觉的。我们在开发的时候,有时候运行一些代码段的时候可能不想进行垃圾回收。这时候我们可以在代码段之前使用System.GC对象中包含的Collect方法,让垃圾回收暂时跳过这些代码。当然,这样做并不会阻止垃圾回收运行,只是降低了这部分代码可能被回收的概率,但是这里有一个前提:代码段执行过程中,不会消耗内存并且大量使用。.NET中垃圾收集的一个特别之处在于,并非所有垃圾都会在垃圾收集周期中收集。为什么是这样?因为.NET垃圾回收器中有一个概念叫generation,翻译成中文就是generation。它会清理寿命较短的对象,而在垃圾回收周期中存活下来的对象清理频率较低。也就是说,当一个对象在一个垃圾回收周期中存活下来时,它就会被移到下一代,如果它在另一个垃圾回收周期中存活下来,它就会被移到最后一代,也就是第一代。第二代(为什么是第二代?因为.NET垃圾回收机制中这一代是从0开始的),第零代清理速度最快,第二代清理速度最慢。弱引用弱引用这个词很少被开发者听到。所谓的弱引用是为创建和维护昂贵的对象而设计的。它不会阻止垃圾收集器回收对象,但它会维护一个可以在被垃圾收集器回收之前重用的引用。例如,我们从数据库中查询一个巨大的数据列表来展示给用户。如果用户在没有使用弱引用的情况下关闭列表,那么垃圾收集器很可能会回收它。那么当用户再次查看该列表时,该程序还需要从数据库中查询和加载,这是非常昂贵的。如果使用引用,每次请求列表时,代码首先检查列表是否被清除,如果没有被清除,则直接将列表显示给用户,如果被清除,则从数据库中查询和显示给用户,相当于缓存在内存中的对象。如果开发者认为该对象应该被弱引用,那么可以将该对象赋值给System.WeakReference。我们来看一个简单的弱认证的例子:WeakReferenceData;publicFileStreamDate(){FileStreamfs=(FileStream)Data.Target;if(data!=null){returndata;}//morecodeData.Target=data;returndata;}上面的代码是创建弱引用的标准代码。我们可以看到代码中变量data被判断为null。我们可以通过这个判断来检查垃圾收集器是否回收了它。这里还有一个关键代码FileStreamfs=(FileStream)Data.Target;这里将弱引用赋值给强引用,可以避免弱引用在检查null之后访问数据之前被垃圾回收器回收。2.资源清理上一节开头我们说了垃圾回收是回收内存中的对象,那么如果我们需要回收其他资源,比如数据库连接、句柄、外部设备等怎么办。这时候我们就需要用到资源清理。Finalizer终结器是允许开发人员通过代码清理类资源的东西。终结器最大的特点是不能在代码中显式调用。只有垃圾收集器负责调用对象实例的终结器。因此,开发者无法在编译时确定finalizer什么时候执行,而只能确定什么时候finalizer是对象。最后一次被调用的地方。finalizer的定义也很简单,就是在类名前加一个~符号即可。classDemo{publicDemo(stringname){//morecode}~Demo(){Close();}publicvoidClose(){//morecode}//morecode}上面的代码定义了一个简单的终结器,我们定义终结器这里有四个东西要请记住:因为终结器是在自己的线程中执行的,所以很难诊断终结器中未处理的异常,因为导致异常的情况尚不清楚。所以我们必须避免在终结器中抛出异常。终结器不允许传递任何参数,也不能重载;因为它是由垃圾收集器调用的,所以为终结器添加访问修饰符是没有意义的;如果父类中有终结器,则将作为子类终结器的一部分自动调用;终结器必须显式释放资源。当我们忘记显式调用必要的清理代码时,使用终结器可以帮助我们执行清理,但由于终结器操作是非确定性的,我们只能将其用作回退机制。通常我们可以使用using。C#中IDisposable接口的Dispose方法为我们提供了实现细节。我们先来看一段代码。classDemo{MyFileStreamfs=newmyFileStram();//morecodefs.Dispose();//morecode}classMyFileStream:IDisposable{publicMyFileStream(stringpath){//morecode}//morecode~MyFileStream{Dispose(false);}publicvoidClose(){Dispose();}publicvoidDispose(){Dispose(true);System.GC.SuppressFinalize();}publicvoidDispose(boolpara){//morecode}}在上面的代码中,我们显式调用了MyFileStream类的Dispose方法。Dispose方法主要是用来清理已经使用过的资源,但是这里有个问题。当我们调用Dispose方法时,可能会发生异常。这时候我们就不能正确的调用Dispose方法了。为了避免这个问题,我们需要添加try。.finally块。但是我们不能保证开发者每次都会写try...finally。这时候我们可以使用C#提供的using语句。让我们修改上面的调用代码:classDemo{using(MyFileStreamfs=newmyFileStram()){//morecode}}该代码最终生成CIL代码,就像使用try...finally块生成的一样。垃圾收集、终结和IDisposable在上一节的代码中,我们看到我们调用了System.GC.SuppressFinalize();在Dispose方法中,其功能是从终结队列中删除MyFileStream实例。因为所有的清理都是在Dispose方法中完成的,而不是等待终结器执行。如果不调用System.GC.SuppressFinalize()方法,该实例会一直在finalization队列中,只有调用finalization方法后才能在垃圾回收器中回收,造成垃圾回收处理时间的延迟的托管资源。在Dispose方法中调用了Dispose(boolpara)方法,在这个方法中我们可以清理资源并防止终结器。其次,我们定义Close方法来调用Dispose(boolpara)方法,这样终结器就可以调用Dispose(boolpara)方法来关闭和释放资源。对于上一节总结的代码,需要注意以下几点:在某些特殊情况下,垃圾回收对象可能会不经意地重新引用到一个挂起的对象。这样,被重新引用的对象就不再不可访问了,也就不能被垃圾回收了。如果对象的终结器已经运行,则终结器不一定会再次运行,除非明确标记为终结。只为高开销和高成本的对象实现终结器;如果该类有终结器,则它必须实现IDisposable;不要在终结器中抛出异常;必须在Dispose方法中调用System.GC.SuppressFinalize;确保Dispose可以被重用;保证Dispose方法的简单性;不能调用其他没有在终结器中终结的对象;如果父类有终结器,重写时必须调用父类终结器;调用Dispose方法后,对象设置为不可用。3.小结本文详细讲解了垃圾回收和资源清理相关的知识。对于一些开发者来说,这部分知识可能比较难理解,但是只要在实际项目中使用,相信是可以很快掌握和理解的。作者简介:朱刚,笔名苗叔,国内某技术博客认证专家,.NET高级开发工程师。曾就职于初创公司,从事企业级安全监控系统开发。【原创稿件,合作网站转载请注明原作者和出处为.com】