自动内存管理 21.8-21.21

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自动内存管理 21.8-21.21相关的知识,希望对你有一定的参考价值。

21.8 Dispose模式:强制对象清理资源

Finalize方法非常有用,因为它确保了当托管对象的内存被释放时,本地资源不会泄漏。但是,Finalize方法的问题在于,他的调用时间不能保证。另外,由于他不是公共方法,所以类的用户不能显式调用它。

类型为了提供显式进行资源清理的能力,提供了Dispose模式。所有定义了Finalize方法的类型都应该同时实现Dispose模式,使类型的用户对资源的生存期有更多的控制。

类型通过实现System.IDisposable接口的方式来实现Dispose模式:

public interface IDisposable
{
        void Dispose();
}

任何类型只有实现了该接口,将相当于声称自己遵循Dispose模式。无参Dispose和Close方法都应该是公共和非虚的。

21.9使用实现了Dispose模式的类型

FileStream类实现了System.IDisposable接口。

            FileStream fs = new FileStream();
            //显示关闭文件 Dispose/Close
            fs.Dispose();
            fs.Close();
            fs.Write();
    

显示调用一个类型的Dispose或Close方法只是为了能在一个确定的时间强迫对象执行清理。这两个方法并不能控制托管堆中的对象所占用的内存的生存期。这意味着即使一个对象已完成了清理,仍然可在它上面调用方法,但会抛出ObjectDisposedException异常。

21.10 C#的using语句

如果决定显式地调用Dispose和Close这两个方法之一,强烈建议把它们放到一个异常处理finally块中。这样可以保证清理代码得到执行。

C#提供了一个using语句,这是一种简化的语法来获得上述效果。

using(FileStream fs = new FileStream())
{
                fs.Write();
}

在using语句中,我们初始化一个对象,并将它的引用保存到一个变量中。然后在using语句的大括号内访问该变量。编译这段代码时,编译器自动生成一个try块和一个finally块。在finally块中,编译器会生成代码将变量转型成一个IDispisable并调用Dispose方法。显然,using语句只能用于哪些实现了IDisposable接口的类型。

21.12手动监视和控制对象的生存期

CLR为每一个AppDomain都提供了一个GC句柄表 (GC Handle table) 。该表允许应用程序监视对象的生存期,或手动控制对象的生存期。

在一个AppDomain创建之初,该句柄表是空的。句柄表中的每个记录项都包含以下两种信息:一个指针,它指向托管堆上的一个对象;一个标志(flag),它指出你想如何监视或控制对象。

为了在这个表中添加或删除记录项,应用程序要使用System.Runtime.InteropServices.GCHandle类型。

21.13对象复活

前面说过,需要终结的一个对象被认为死亡时,垃圾回收器会强制该对象重生,使它的Finalize方法得以调用。Finalize方法调用之后,对象才真正的死亡。

需要终结的一个对象会经历死亡、重生、再死亡的“三部曲”。一个死亡的对象重生的过程称为复活(resurrection) 。复活一般不是一件好事,应避免写代码来利用CLR这个“功能”。

21.18   线程劫持

前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。

在现实开发中,经常会出现多个线程同时访问托管堆的情况,或至少会有多个线程同时操作堆中的对象。一个线程引发垃圾回收时,其它线程绝对不能访问任何线程,因为垃圾回收器可能移动这些对象,更改它们的内存位置。

CLR想要进行垃圾回收时,会立即挂起执行托管代码中的所有线程,正在执行非托管代码的线程不会挂起。然后,CLR检查每个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。

如果线程的指令指针恰好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。如果线程指令指针不在表中标记的偏移位置,则表明该线程不在安全点,CLR也就不会开始垃圾回收。在这种情况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。然后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。

然而,线程有时长时间执行当前所在方法。所以,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。如果线程已抵达一个安全点,垃圾回收就可以开始了。但是,如果线程还没有抵达一个安全点,CLR就检查是否调用了另一个方法。如果 是,CLR再一次修改线程栈,以便从最近执行的一个方法返回之后劫持线程。然后,CLR恢复线程,进行下一次劫持尝试。

所有线程都抵达安全点或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。

实际应用中,CLR大多数时候都是通过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之所以如此,原因是JIT生成表需要大量内存,会增大工作集,进而严重影响性能。

21.20大对象

任何85000字节或更大的对象都被自动视为大对象(large object)

大对象从一个特殊的大对象堆中分配。这个堆中采取和前面小对象一样的方式终结和释放。但是,大对象永远不压缩(内存碎片整理),因为在堆中下移850000字节的内存块会浪费太多CPU时间。

大对象总是被认为是第2代的一部分,所以只能为需要长时间存活的资源创建大对象。如果分配短时间存活的大对象,将导致第2代被更频繁地回收,进而会损害性能。

以上是关于自动内存管理 21.8-21.21的主要内容,如果未能解决你的问题,请参考以下文章

C# Dispose Finalize

C#-C#中的Dispose模式

Datatable.Dispose() 会使其从内存中删除吗?

发现 .NET 内存泄漏?

RxSwift 为啥我们没有调用 dispose 会有内存泄漏

System.NewSystem.Dispose - 为某个指针申请和释放内存