C#笔记——垃圾回收与资源管理

本文最后更新于:2023年11月12日 晚上

垃圾回收与资源管理

析构器

其实就是析构方法。语法就是类名前加一个~,比如~ClassName(){ ... }
最重要的一点,析构器只有在对象被垃圾回收时才运行
编译器内部自动将析构器转换成对Object.Finalize方法的一个重写版本的调用,比如:

1
2
3
4
class ClassName
{
~ClassName() { ... }
}

就等价于:

1
2
3
4
5
6
7
8
class ClassName
{
protected override void Finalize()
{
try { ... }
finally { base.Finalize(); }
}
}

垃圾回收

垃圾回收什么时候进行?

可以确定的是,在对象不再需要的时候进行。但是垃圾回收不一定在对象不再需要之后立刻进行CLR会自行判断执行垃圾回收的时机。
例如,在它认为可用内存不够的时候,或者堆的大小超过系统定义的阈值的时候。

垃圾回收器的工作原理

大体步骤如下:

  1. 构造所有可达对象的一个映射(map)。为此,它会反复跟随对象中的引用字段。垃圾回收器会非常小心地构造映射,确保循环引用不会造成无限递归。任何不在映射中的对象肯定不可达。
  2. 检查是否有任何不可达对象包含一个需要运行的析构器(运行析构器的过程称为“终结”)。需终结的任何不可达对象都放到一个称为freachable(F-reachable)的特殊队列中。
  3. 回收剩下的不可达对象(即不需要终结的对象)。为此,它会在堆中向下面移动可达的对象,对堆进行“碎片整理”,释放位于堆顶部的内存。一个可达对象被移动之后,会更新对该对象的所有引用。
  4. 然后,允许其他线程恢复执行。
  5. 在一个独立的线程中,对需要终结的不可达对象执行终结操作。

资源管理

当我们使用某些资源时,比如内存、数据库、文件句柄等这种稀缺资源,应该尽快释放,所以这个时候就需要我们手动释放资源。
即通过自己写的资源清理(disposal)方法来实现资源释放。可显式调用类的资源清理方法,从而控制释放资源的时机。
简单举个例子,比如我们使用TextReader类来从顺序输入流读取字符时,读取完毕后应该及时调用Close()方法释放资源:

1
2
3
4
5
6
7
TextReader reader = new StreamReader(filename);
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
reader.Close();

但是它其实不是异常安全的,因为如果在reader.ReadLine()或其他地方出现异常的话,就不会执行reader.Close()释放文件句柄了。
或许我们会想到try...catch...finally...语句,这确实是一种解决方法,然而也存在更好的方案。

using语句

using语句提供了一个脉络清晰的机制来控制资源的生存期。可以创建一个对象,这个对象会在using语句块结束时销毁。
using语句的用法如下:

1
2
3
4
using (type variable = initialization)
{
statementBlock
}

它等价于下面的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
type variable = initialization;
try
{
statementBlock
}
finally
{
if (variable != null)
{
((IDisposable)variable).Dispose();
}
}
}

这也说明using语句声明的变量必须实现IDisposable接口。IDisposable接口在System命名空间中,只包含一个名为Dispose的方法。Dispose方法的作用是清理对象使用的任何资源。

IDisposable接口的实现

下面是实现IDisposable接口的类的推荐写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Example : IDisposable
{
private Resource scarce; // 要管理和清理的稀缺资源
private bool disposed = false; // 指示资源是否已被清理的标志
...
~Example()
{
this.Dispose(false);
}

public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (this.disposing)
{
// 在此释放大型托管资源
...
}
// 在此释放非托管资源
...
this.disposed = true;
}
}

public void SomeBehavior() // 示例方法
{
checkIfDisposed(); // 每个常规方法都要调用这个方法来检查对象是否已经清理
...
}

private void checkIfDisposed()
{
if (this.disposed)
{
throw new ObjectDisposedException("对象已经清理!");
}
}
}

下面解释一下这么写的好处:

  • 受保护的Dispose方法可以安全地多次调用,因为变量disposed指出方法以前是否运行过,这样可以防止在并发调用方法时资源被多次清理。
  • 受保护的Dispose方法支持托管资源(比如大的数组)和非托管资源(比如文件句柄)的清理。如果disposing参数为true,该方法肯定是从公共Dispose()方法中调用的,所以托管和非托管资源都会被释放。如果disposing参数为false,该方法肯定是从析构器中调用的,而且垃圾回收器正在终结对象,所以不需要释放托管资源,因为它们将由(或者已经由)垃圾回收器处理:在这种情况下只需要释放非托管资源。
  • 静态GC.SuppressFinalize方法可以阻止垃圾回收器为这个对象调用析构器,因为对象已经终结

这里还有另外一种线程安全的方式写代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void Dispose()
{
lock(this)
{
if (!disposed)
{
...
}
this.disposed = true;
GC.SuppressFinalize(this);
}
}

lock语句旨在阻止一个代码块同时在不同线程上运行。像这样使用锁能确保线程安全,但对性能有一些影响。


这里有一只爱丽丝

希望本文章能够帮到您~


C#笔记——垃圾回收与资源管理
https://map1e-g.github.io/2023/08/14/CSharp-learning-2/
作者
MaP1e-G
发布于
2023年8月14日
许可协议