参考书:《 visual C# 从入门到精通》
第二部分 理解C#对象模型
第14章 使用垃圾回收和资源管理
文章目录
- 14.1 对象的生存期
- 14.1.1 编写析构器
- 14.1.2 为什么要使用垃圾回收器
- 14.1.3 垃圾回收器的工作原理
- 14.1.4 慎用析构器
- 14.2 资源管理
- 14.2.1 资源释放方法
- 14.2.2 异常安全的资源清理
- 14.2.3 `using`语句和`IDisposable`接口
- 14.2.4 从析构器中调用`Dispose`方法
- 14.3 实现异常安全的资源清理
14.1 对象的生存期
我们知道,创建对象要用new
关键字,
Square mysquare = new Square();
new
实际上是分两步的:
-
new
操作从堆中分配原始内存,这个阶段无法进行干预 -
new
操作将原始内存转换成对象:它必须初始化对象。可用构造器控制这一阶段
当变量mysquare
离开作用域时,它引用的对象就没有引用了,这样对象会被销毁,占用的内存被回收。对象的销毁也分来两步走:
1. `CLR`执行清理工作,可以写一个析构器来控制
2. `CLR`将对象占用的内存归还给堆,解除对象内存的分配。对这个阶段我们没有控制权。
销毁对象并将内存归还给堆的过程称为垃圾回收。
14.1.1 编写析构器
用析构器可以在对象被垃圾回收时执行必要的清理。CLR
可以自动清理对象使用的任何托管,所以很多时候时不需要自己写析构器的。但要是托管资源很大(如多维数组),就可以考虑对该资源的所有引用都设为null
,使资源能被立即清理。如果对象引用了非托管资源,析构器就更有用了。
构造器的语法是~
+类名,如下:
class FileProcessor{
FileStream file=null;
public FileProcessor(string fileName){
this.file=File.OpenRead(fileName);
}
~FileProcessor(){
this.file.Close();
}
}
注意以下几点:
- 构造器只适合引用类型。值类型不能声明构造器(如
struct
) - 不能为构造器指定访问修饰符。因为总是由垃圾回收器来调用
- 析构器不能获取任何参数
编译器内部自动将构造器转换成对Object.Finalize
方法的一个重写版本的调用。如:
class FileProcessor{
-FileProcessor(){//你的代码}
}
编译器转换成如下的形式:
class FileProcessor{
protected override void Finalize(){
try{//你的代码
}
finally{base.Finalize();}
}
}
14.1.2 为什么要使用垃圾回收器
C#中我们总是不能自己销毁对象,而是由CLR
在它认为合适的时间帮你做这件事。由于一个对象可能会有多个引用,CLR
必须跟踪所有的引用。对象的生存期不能和特定的引用变量绑定。只有在一个对象的所有引用都消失后才可以销毁该对象,回收其内存。
如果由程序员负责销毁对象,很有可能会遇到以下的情况:
- 忘记销毁对象。这样析构器不会运行,清理工作也不会运行,内存不会回到堆,最终内存会被消耗完
- 试图销毁活动对象,造成一个或多个变量容纳对已销毁对象的引用,即虚悬引用
- 试图多次销毁同一个对象
而垃圾回收器可以做到以下几点担保:
- 每个对象都会被销毁
- 每个对象只能被销毁一次
- 每个对象只有在它不可达时销毁
注意,垃圾回收并不是在对象不再需要时立即进行。垃圾回收是一个代价较高的过程,所以只在觉得有必要时才会进行垃圾回收。
可以通过静态方法System.GC.Collect
在程序中调用垃圾回收器(回收过程时异步发生的),但不建议这样做。
14.1.3 垃圾回收器的工作原理
垃圾回收器在它自己的线程中运行。它运行时应用程序中的其他线程将暂停,因为垃圾回收器可能需要移动对象并更新对象引用。
它的步骤大体如下:
- 构造所有可达对象的一个映射,它会反复跟随对象中的引用字段
- 检查是否由任何不可达对象包含一个需要运行的析构器,需终结的任何不可达对象都放到一个称为
freachable
的特殊队列中 - 回收剩下的不可达对象,它会在堆中向下面移动可达的对象,对堆进行“碎片整理”,释放位于堆顶部的内存。一个可达对象被移动之后会跟新对该对象的所有引用
- 然后允许其他线程恢复执行
- 在一个独立的线程中,对需要终结的不可达对象执行终结操作、
14.1.4 慎用析构器
一个类中包含析构器会使代码和垃圾回收过程变得复杂,同时会影响程序的运行速度。如果程序中不包含析构器,垃圾回收器就不需要将不可达对象放到freachable
队列并对他们进行“终结”。所以除非确实有必要,最好尽量避免使用析构器。同时要确定析构器不相互依赖或相互重叠。
14.2 资源管理
有点资源比较宝贵,用完后应该马上释放,这时就需要通过自己写的资源清理方法亲自释放资源。显式调用类的资源清理方法控制资源是释放的时机。
14.2.1 资源释放方法
比如来自System.IO
命名空间中的TextReader
类,该类提供从顺序输入流中读取字符的机制。TextReader
包含虚方法Close
,它负责关闭流,这是一个资源清理方法。StreamReader
类从流中读取字符,StringReader
类从字符串中读取字符。这两个类都从TextReader
类派生,都重写了Close
方法。如使用StreamReader
类从文件中读取文本行并在屏幕上显示:
TextReader reader =new StreamReader(filename);
string line;
while((line=reader.ReadLine())!=null){
Console.WriteLine(line);
}
reader.Close();
上述代码的非常重要的一点是调用Close
方法来释放文件句柄以及相关资源。但有一个问题是它不是异常安全的。如果对ReadLine
或WriteLine
方法的调用抛出异常,对Close
的调用就不会发生了,这样可能会耗尽文件句柄资源,无法打开更多文件。
14.2.2 异常安全的资源清理
为了解决上面的问题,可以在finally
块中调用Close
方法:
TextReader reader=new StreamReader(filename);
try{
string line;
while((line=reader.ReaderLine())!=null){
Console.WriteLine(line);
}
}
finally{
reader.Close();
}
但它存在几个缺点:
- 如果要释放多个资源,局面就会很混乱,可能出现嵌套的
try
和finally
- 有时需要修改代码来适应这一惯用法
- 它不能创建解决方案的一个抽象
- 对资源的引用保留在
finally
块之后的作用域中,这意味着可能不小心使用一个已释放的资源
14.2.3 using
语句和IDisposable
接口
using
语句提供了一个脉络清晰的机制来控制资源的生存期。语法如下:
using(type variable=initialzation){
...;
}
如:
using(TextReader reader=new StreamReader(filename)){
string line;
while((line=reader.ReadLine())!=null){
Console.WriteLine(line);
}
}
它完全等价于前面用finally
块的形式:
{
TextReader reader=new StreamReader(filename);
try{
string line;
while((line=reader.ReaderLine())!=null){
Console.WriteLine(line);
}
}
finally{
reader.Close();
}
}
using
语句定义了一个作用域。
using
语句声明的类型必须实现IDisposable
接口。IDisposable
在System
命名空间中,只包含一个名为Dipose
的方法。
namespace System{
interface IDisposable{
void Dispose();
}
}
而StreamReader
类实现了这个接口,它的Dispose
方法会调用Close
方法来关闭流。using
语句解决了finally
块可能出现的所有问题。
14.2.4 从析构器中调用Dispose
方法
实现IDisposable
接口的示例如下:
Class Example:IDisposable{
private Resource scarce;
private bool dispose=false;
...;
~Example(){
this.Dispoable(false);
}
public virtual void Dispose(){
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing){
if(!this.disposed){
if(disposing){
...;
}
this.disposed=true;
}
}
public void SomeBehavior(){
checkIfDisposed();
...;
}
private void checkIfDisposed(){
if(this.disposed){
throw new ObjectDisposedException("示例:对象已经清理");
}
}
}
注意:
- 受保护的
Dispose
方法可以安全的多次调用。变量disposed
指出方法以前是否运行过,这样防止在并发调用方法时资源被多次清理。方法只有在第一次运行才会清理资源 - 受保护的
Dispose
方法支持托管资源和非托管资源的清理 - 公共
Dispose
方法调用静态GC.SuppressFinalize
方法。该方法阻止垃圾回收器为这个对象调用析构器,因为对象已经终结了 - 类的所有常规方法都要检查对象是否已经被清理,是,就抛出异常
14.3 实现异常安全的资源清理
最后放上一个using
语句的示例:
Calculator.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace C_14_3
{
class Calculator:IDisposable
{
private bool disposed = false;
public int Divide(int first,int second)
{
return first / second;
}
public void Dispose()
{
if (!disposed)
{
Console.WriteLine("Calculator being disposed");
}
this.disposed = true;
GC.SuppressFinalize(this);
}
public Calculator()
{
Console.WriteLine("Calculator being created");
}
~Calculator()
{
Console.WriteLine("Calculator being finalized");
this.Dispose();
}
}
}
Program.cs
using System;
namespace C_14_3
{
class Program
{
static void Main(string[] args)
{
using(Calculator calculator1=new Calculator())
{
Console.WriteLine($"120/0={calculator1.Divide(120, 0)}");
}
Console.WriteLine("Program finishing");
}
}
}
using
语句中故意引发异常,观察结果,程序最后还是调用了Dispose
方法来回收资源,运行结果为