C#的垃圾回收机制

雨燕双飞 提交于 2019-12-30 04:49:33

前言

大家都知道使用高级语言的时候,我们不必担心内存回收的问题,因为有垃圾回收器会自动处理所有内存,虽然不必自己手动去管理内存,但是我们还是需要详细去理解一下垃圾回收机制到底发生什么事情?

1.栈存储

在处理器的虚拟内存中,有一个区域称为栈,栈存储的是值数据类型。另外,在调用一个函数的时候,函数的参数是使用栈副本的拷贝,可能有人说了不是有引用传递吗?这么感觉你说的好像只有值传递,请客官看完这篇文章相信你就明白了。为了理解栈的工作原理,需要知道变量作用域是什么,这个自行百度...如果变量a在变量b之前进入作用域,b就首先超出作用域,b也就首先释放,请看如下代码:

{
    int a;
    {
        int b;
        //TODO doSomething
    }

}

首先声明变量a,接着在内部代码声明变量b,然后内部代码块结束地时候,b就超出作用域,最后a也超出作用域,在释放变量时,栈会按照这个顺序释放内存,与分配内存的顺序相反,先分配的最后释放,这就是栈的工作方式。

程序第一次开始运行时,栈指针指向为栈保留的内存块尾端,栈实际是向下填充的,即高内存地址向低内存填充,当数据入栈,栈指针就会随之调整,以始终向下空闲存储单元,要存储一个int型的值,为了容纳下该int型,应从栈指针往下减去4,double型是往下减去8,当某个值类型超出作用域以后(比如int型),为了从内存中删除这个变量,应该往上增加4即可,如果有新数据直接往上覆盖就行了,所以当我们使用递归的时候如果没有弹栈条件(也就是递归压栈时那些值类型没有办法超出作用域),就会导致栈溢出。

如果编译器遇到int i,j这样的代码行,则这个2个变量进入作用域的顺序是不确定,2个变量是同时声明的,也是同时超过作用域的,此时变量是什么顺序从内存中删除就不重要了,编辑器会确保先放在内存中的那个变量后删除,还是会按照那个规则执行,所以不必担心会出现任何问题。

2.堆存储

尽管栈有非常高的的性能,所以如果可以使用结构体比使用类性能更高,但它还没有灵活到可以用到所有变量。变量的生存期必须嵌套,这个要求很过于苛刻,通常我们希望使用一个函数分配内存,来存储一些数据,并在方法退出之后一段时间还是可用。如下代码:

{
    SampleClass a = new SampleClass();
    SampleClass b = new SpecialClass(); 
}

首先一个SampleClass引用a是在栈上分配存储空间,但是这单单是一个引用地址,而不是实际的数据,a的引用占用的空间是4字节(4字节可以把0~4G之间的内存地址表示为一个整数值),a的实例没有放在栈中,而是放到堆中。我们建设a的实例空间是32字节,下面是实例图:

可以看出引用变量创建的过程比值变量要更加复杂,性能开销一定就更加的高,在给变量分配内存时,不会收到栈的限制,把一个引用变量的值赋给类一个相同的变量,它们的引用地址是一样的,共有一个堆空间上的数据,所以引用传递和值传递其实都是栈的副本拷贝,当引用变量超过作用域以后,它也会释放,当没有引用变量指向这个堆空间时,垃圾回收机制将把堆上的数据释放掉。

3.垃圾回收机制

托管堆的工作方式类似于栈,对象一个挨着一个进行放置,这样指向下一个的就是空闲的存储单元,但是我们思考一下堆上对象的生存期是和在栈上引用透明的变量有关,所以会导致堆不能像栈一样有顺序的释放。当垃圾回收器开始GC的时候,会把不再引用的所有对象释放掉,这样堆上的对象就会分散开了,如果下次还需要存储新的对象,必须要搜索整个堆才可以进行分配空间。所以当垃圾回收器GC过一次之后,它会把其他没有释放的对象移动到堆的端部,再次形成连续的存储空间,移动完以后,栈上的那些引用变量都需要重新引用正确的新地址,这些问题垃圾回收机制都会处理。当然我们可以自己手动的回收调用System.GC.Collect()函数,强制进行一次垃圾回收的生命周期,一般不需要咱们手动的调用(GC也是需要消耗性能的,所以程序需要适当的时候进行调用),但是当代码有大量的对象刚刚取消引用,就很适合调用这个接口去释放一次托管堆。这个设计很合理,不仅仅可以提高分配堆的性能也可以提高释放不必要的堆数据。垃圾回收机制添加了Settings.LatencyMode属性,设置这个枚举可以控制垃圾回收的方式。

Settings.LatencyMode设置
Batch 禁用并发设置,把垃圾回收设置为最大的吞吐量。这会重写配置设置
Interactive 工作站的默认行为。它使用垃圾回收并发设置,平衡吞吐量和响应
LowLatency 保守的垃圾回收,只有系统内存压力时,才会进行完整的回收,只应用于比较短的时间,执行特定的操作
SustainedLowLatency 只有系统内存压力时,才会进行完整的回收
NoGCRegion 可以在代码中调用GC.TryStartNoGCRergion和EndNoGCRegion来设置它,调用TryStartNoGCRegion,定义需要可用的,GC试图访问内存大小,成功调用TryStartGCRegion之后,指定不应运行的垃圾回收器,直到调用EndNoGCRegion为止

 

 

 

 

 

 

 

 

4.强引用和弱引用

垃圾回收器不能收回依然存在的引用叫强引用,它可以回收不在根表直接或者间接的引用的托管引用内存,但是,有时还是可能会忘记释放引用。通俗一点来说如果有代码还在其作用域之内引用这个对象,就会形成强引用。但是强引用何时会忘记释放呢?请各位看如下的代码:

var myCache = new MyCahce();
myCache.Add(myClassVar);
myClassVar = null;

如果垃圾回收现在运行,就不能释放myClassVar引用的内存,因为该对象还存在缓存对象的引用(大家应该没有忘记引用传递其实也是栈上的引用副本拷贝),这个引用很容易忘记,所以使用WeakReference可以避免出现这个问题。弱引用允许创建和使用对象,但是垃圾回收器碰巧在运行,就会回收对象并释放内存,由于存在潜在的bug和性能问题,一般不会咱们做,但是在特定的情况下使用弱引用是很合理的,弱引用对小对象也没有意义,因为弱引用也有自己都开销,这个开销可能对小对象还要大。弱引用使用WeakRefence类来创建对象,可以传递强引用,可以调用IsAlive属性查看对象是否存活,WeakReference的Target的返回一个强引用。如果返回null,就需要重新创建,因为对象可能任何时刻被回收,所以引用它之前保证它存在,具体代码如下:

var myWeakRefVar = new WeakReference(new MyClass());
if(myWeakRefVar.IsAlive)
{
    DataObject strongRef = myWeakRefVar.Target as DataObject;
    if(strongRef != null)
    {

    }
}
else
{
    //重新创建
}

5.非托管资源

非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这垃圾回收器在清理的时候会调用Object.Finalize()方法或者在类中继承实现IDisposable接口。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。在垃圾回收机制中也会调用Object.Finalize()方法,C#的析构函数会延迟对象最终删除的时间,没有析构函数的对象会一次删除内存,有析构函数的对象需要两次才能销毁,第一次调用析构函数没有删除对象,第二次才开始删除对象,而且频繁使用它去清理长时间的任务,对性能影响严重。

所以推荐Disposable接口来替代析构函数(Finalize),Dispose()函数的实现代码显式释放由对象直接使用的所有非托管堆资源,并在所有实现IDisposeable接口对象上调用Dispose(),这样Dispose()释放非托管堆资源就可以准确的控制,如下代码:

class MyClass:IDispose
{
    public void Dispose()
    {
        
    }
}

static void Main(string[] args)
{
    try
    {
        MyClass myClass = new MyClass();
    }
    finally
    {
        myClass?.Dispose();
    }
}

以下代码和上述的try/finally一个效果:

using(var myClass = new MyClasss)
{
    //使用这个对象做点什么,不用担心释放
}

如果创建终结器,就要实现IDisposable接口,可以同时实现析构函数作为一种安全机制,防止调用Dispose(),下面是双重实现的例子:

using System;

public class ResourceHolder: IDispose
{
    private bool _isDisposed = false;
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);//告诉垃圾回收机制,这个类不再需要调用析构函数,因为Dispose已经完成清理了,调用以后垃圾回收器会认为这个类没有析构函数
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if(!_isDisposed)
        {
            if(disposing)
            {
                
            }    
        }
        _isDisposed = true;
    }

    ~ResourceHolder()
    {
        Dispose(false);
    }

    public void Method()
    {
        if(_isDisposed)
        {
            throw new ObjectDisposedException("ResourceHolder");
        }
    }
}

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!