.NET中C#堆VS栈:Part IV

描述

 

让我们从GC的视角看一下这个问题。如果要可靠的找出垃圾,我们需要一个高效的方法。显然,我们需要决定哪些是垃圾,哪些不是。为了确定哪些需要保留,首先我们假设所有没有被使用的都是垃圾。设想一下我们有两个朋友:JIT和CLR,他们负责跟踪什么正在使用并且给出一个保存清单。我们称这份清单为根清单,因为我们使用它作为起始点。我们需要保存一个主清单用来描述我们想保留的东西都在哪。那些清单中保留的东西工作时需要依赖的东西也会被添加到清单中。(比如:如果我们保留电视,我们肯定不能丢掉遥控器,所以遥控器也要保留到清单中)

这也是GC的保留策略。它从JIT和CLR哪里接收并保存一个根对象的清单,并且递归查找根对象的引用形成一个保留图表。

根的组成:

  • 全局/静态 指针。一种方式确保我们的对象不被垃圾回收期回收的方式,就是使用静态变量
  • 栈中的指针。在栈中正在执行的程序肯定不希望被丢掉
  • CPU寄存器指针。在托管堆中正在被CPU引用的对象需要被保留

在上面的图中,objects1,3,和5在托管堆中被根对象引用,1和5是直接引用,3是在递归查找时被发现。就像之前的比喻,如果1是电视机的话,那么3就是遥控器。在所有对象都检查完毕后,我们就准备好了进入下一个环节,压缩。

压缩

现在我们已经把需要保留的对象都记录下来了,需要做的就是把要保留的对象放在一起。

幸运的是在我们需要存放东西之前,并不需要清理。当Object 2不需要时,GC会向下移动Object 3并修正Object1引用Object3的指针。

 

下一步,把Object5挪下来

 

现在所有的东西都清理完毕,我们只需要写一个便签放在压缩好的堆顶部,让大家知道在哪放新的对象。

知道GC的原理,能让我们明白把对象移动到一起是相当繁重的工作。就像你看到的,如果我们可以减小需要移动对象的尺寸,就可以提高GC的处理效率,因为需要移动的东西少了。

非托管堆是什么样的

对于一个负责的垃圾处理者,一个需要使用面对的问题是在清理房间时如何处理汽车中的东西。清理时,我们需要清理所有的东西。如果我们要清理一台笔记本电脑,电池却在车上呢?

这些情形下GC需要执行了代码来清理非托管资源(如:文件,数据库连接,网络连接等等)。一个可行的方式是通过析构函数。

class Sample
{
    ~Sample()
    {
        // FINALIZER: CLEAN UP HERE
    }
}

对象创建时,所有含有析构函数的对象都会被放进一个finalization queue。对象1,4和5包含析构函数并且在finalization queue上。让我们来看一下当object 2和4不再有引用并且已经准备好GC时会发生什么。

Object 2被常规对待,然而当轮到Object 4的时候,GC发现它在finalization queue上,object 4 的析构器被添加到一个叫freachable的queue中。

 

这里有一个专用的线程来执行freachable queue。一旦析构器被这个线程执行,Object 4被从freachable queue中移除。只有这时Object 4才准备好被回收。

 

所以Object 4会一直存在知道下一个GC周期。

因为在类中加入析构函数引起了GC繁重的额外工作,并且对GC的性能乃至我们的程序产生不利的影响。只在不得不使用析构函数的时候使用它。

一个更好的释放非托管资源的方式是实现IDisposable 接口。

IDisposaible

实现IDisposable 的类通过执行Dispose() 方法进行回收(接口中唯一的方法签名)。所以假设有一个类ResouceUser用这种方式替代析构函数,如下:

public class ResourceUser
{
    ~ResourceUser() // THIS IS A FINALIZER
    {
        // DO CLEANUP HERE
    }
}

我们可以用IDisposable 接口更好的实现同样的功能:

public class ResourceUser : IDisposable
{
    #region IDisposable Members
    public void Dispose()
    {
        // CLEAN UP HERE!!!
    }
    #endregion
}

IDisposable接口和using关键字是配合使用的,在using块的最后,Dispose()会被调用。在using块之后,对象不应该再被引用,因为显而易见它应该被GC清理掉。

public static void DoSomething()
{
    ResourceUser rec = new ResourceUser();
    using (rec)
    {
        // DO SOMETHING
    } // DISPOSE CALLED HERE
    // DON'T ACCESS rec HERE
}

我喜欢使用using块,因为它看起来可读性更强,而且变量在块外不可用。

public static void DoSomething()
{
    using (ResourceUser rec = new ResourceUser())
    {
        // DO SOMETHING
    } // DISPOSE CALLED HERE
}

通过使用using和实现IDisposible 接口,使得在GC清理我们的类时没有任何额外的工作。

注意静态变量

class Counter
{
    private static int s_Number = 0;
    public static int GetNextNumber()
    {
        int newNumber = s_Number;
        // DO SOME STUFF
        s_Number = newNumber + 1;
        return newNumber;
    }
}

如果两个线程同时调用GetNextNumber()方法并且都在s_Number增长前赋值给newNumber变量,那么它们将得到相同的结果!一种方式确保同一时间只有一个线程可以访问代码块是使用锁,应该lock尽可能小的代码块,因为其他线程需要等待lock住的代码块执行完。

class Counter
{
    private static int s_Number = 0;
    public static int GetNextNumber()
    {
        lock (typeof(Counter))
        {
            int newNumber = s_Number;
            // DO SOME STUFF
            newNumber += 1;
            s_Number = newNumber;
            return newNumber;
        }
    }
}

再次注意静态变量

下一个要注意的是被静态变量引用的ojbects,记住,被根引用的东西不会被清理。下面是一个特别恶心的例子:

class Olympics
{
    public static Collection<Runner> TryoutRunners;
}

class Runner
{
    private string _fileName;
    private FileStream _fStream;
    public void GetStats()
    {
        FileInfo fInfo = new FileInfo(_fileName);
        _fStream = _fileName.OpenRead();
    }
}

因为Runner集合是 Olympics类的静态变量,不光是它永远不会被GC清理,你可能注意到了,每次我们在执行GetStats()方法时,stream被打开一次。因为它没有关闭也不会被GC清理,这个代码就是个灾难。试想一下执行100000次的后果。

单例

一个技巧是对于通用的类,在内存中永远保持一个实例。这就是GOF中的单例模式。单例模式应该慎用,因为它是全局变量,在多线程编程中,多个线程可以改变它的状态,经常引起一些奇怪的行为和头疼的问题。如果要用单例模式,最好要有充分的理由。

public class Earth 
{
    private static Earth _instance = new Earth();
    private Earth() { }
    public static Earth GetInstance() { return _instance; }
}

上面是单例模式的经典实现方式,并且这种方式是线程安全的。因为CLR确保static变量的线程安全。

结论

提高GC性能的方式:

  1. 清理。不要让资源一直开着。确保所有用完的连接都关闭并且尽快清理非过关资源。一个使用非托管资源的规则,尽量晚的实例化,尽量早的释放。
  2. 不要过度引用,记住,如果object在用,所有它引用的对象都不会清理,以此类推。当使用完一个变量时,可以手动设置它为null,告诉GC它可以被清理。一个小技巧是,可以赋给变量一个轻量级的空Object,以避免出现null reference异常。引用的对象越少,清理时GC需要遍历记录的描述越少。
  3. 少用析构函数,尽量使用实现IDisposible接口和using代码块实现。
  4. 尽量将父和子对象放在一起,GC移动一块大的内存会比处理很多碎片要容易,所以当实例化一个对象时,尽量一起实例化它引用的对象。

原文链接:

https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iv/

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值