65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 最佳实践编号:2:使用 finalize/dispose 模式提高垃圾收集器性能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (77投票s)

2009年8月23日

CPOL

12分钟阅读

viewsIcon

257242

.NET 最佳实践编号:2:- 使用 finalize/dispose 模式提高垃圾收集器性能

已更新 .NET 最佳实践编号:1、3、4 和 5 的链接

.NET 最佳实践编号:2:- 使用 finalize/dispose 模式提高垃圾收集器性能

 

本文是否值得继续阅读?

引言和目标

假设

感谢 Jeffrey Richter 先生和 Peter Sollich 先生

垃圾收集器 – 被忽视的英雄

分代算法 – 今天、昨天和前天

好的,分代是如何帮助优化的?

关于分代的结论

使用 finalize/析构函数会导致 Gen 1 和 Gen 2 中对象增多

通过使用 Dispose 来摆脱析构函数

如果开发者忘记调用 Dispose 怎么办?

结论

源代码

其他实践

我的 FAQ 文章

使用 finalize/dispose 模式提高垃圾回收器性能


 

C# 中关于垃圾收集的重要面试题和答案

本文是否值得继续阅读?
 

通过本文,您将了解如何使用 finalize dispose 模式来提高 GC 算法的性能。下图显示了本文完成后的对比效果。
 

引言和目标
 

问任何一个开发者,在 .NET 类中清理非托管资源的最佳位置是哪里?70% 的人会说是析构函数。虽然它看起来是清理的最佳位置,但它对性能和内存消耗有巨大的影响。在析构函数中编写清理代码会导致 GC 访问两次,从而多次影响性能。

为了验证这一点,我们首先从一些理论开始,然后我们将实际看看 GC 算法的性能如何受到析构函数的影响。所以我们先理解分代的概念,然后我们将了解 finalize dispose 模式。

我确信这篇文章将改变您对析构函数、dispose 和 finalize 的看法。

假设
 

本文使用 CLR profiler 来分析 GC 的工作原理。如果您是 CLR profiler 的新手,请在继续阅读本文之前,先阅读 DOTNET1.aspx
 

感谢 Jeffrey Richter 先生和 Peter Sollich 先生
 

让我们从感谢 Jeffery Richter 先生深入解释 GC 算法的工作原理开始。他撰写了两篇关于 GC 工作方式的经典文章。我本来想引用 MSDN 杂志上 Jeffery Richter 的文章,但由于某种原因,它在 MSDN 上没有显示。所以我指向了一个不同的非官方位置,您可以从 http://www.cs.inf.ethz.ch/ssw/files/GC_in_NET.pdf 下载这两篇文章的 PDF 格式。

同时感谢 CLR 性能架构师 Peter Sollich 先生撰写了关于 CLR profiler 的详细帮助文档。安装 CLR profiler 时,请务必阅读 Peter Sollich 撰写的详细帮助文档。在本文中,我们将使用 CLR profiler 来检查 finalize 如何影响 GC 的性能。

非常感谢你们。没有你们的文章,我不可能完成这篇文章。你们任何时候经过这篇文章,请务必评论,我很想听听你们的看法。
 

垃圾收集器 – 被忽视的英雄
 

正如引言中所述,在析构函数中编写清理代码会导致 GC 访问两次。许多开发者可能想置之不理,说“我们真的应该担心 GC 和它在幕后所做的事情吗?”。是的,如果您编写的代码**得当**,您确实不应该担心 GC。GC 有最佳算法来确保您的应用程序不受影响。但很多时候,您编写代码的方式以及在代码中分配/清理内存资源的方式会极大地影响 GC 算法。有时这种影响会导致 GC 性能不佳,从而导致应用程序性能不佳。

所以,让我们先了解一下垃圾收集器在应用程序中分配和清理内存时执行的各种任务。

假设我们有 3 个类,其中类 ‘A’ 使用类 ‘B’,类 ‘B’ 使用类 ‘C’。
 

当应用程序首次启动时,会为应用程序分配预定义的内存。当应用程序创建这 3 个对象时,它们会在内存堆中分配一个内存地址。您可以在下图看到对象创建前的内存样子以及对象创建后的内存样子。如果创建了对象 D,它将从对象 C 结束的地址分配。

GC 内部维护一个对象图,以了解哪些对象是可访问的。所有对象都属于主应用程序根对象。根对象还维护哪个对象在哪个内存地址上分配。如果一个对象使用了其他对象,那么该对象还存储了已使用对象的内存地址。例如,在本例中,对象 A 使用对象 B。所以对象 A 存储了对象 B 的内存地址。

现在,假设对象‘A’已从内存中移除。所以对象‘A’的内存被分配给对象‘B’,对象‘B’的内存被分配给对象‘C’。因此,内部内存分配看起来如下。

在更新地址指针的同时,GC 也需要确保其内部图已使用新的内存地址进行更新。所以对象图变成了如下所示。这对 GC 来说是一项繁重的工作,它需要确保对象已从图中移除,并且整个对象树中已更新现有对象的地址。

除了自定义对象外,应用程序还包含 .NET 对象,这些对象也构成了图的一部分。这些对象的地址也需要更新。.NET 运行时对象数量非常多。例如,下面是一个简单的基于控制台的 hello world 应用程序创建的对象数量。对象数量大约在数千个。更新每个对象的指针是一项巨大的任务。

分代算法 – 今天、昨天和前天
 

GC 使用分代的概念来提高性能。分代概念基于人类心理处理任务的方式。以下是一些关于人类如何处理任务以及 GC 算法如何在此基础上工作的要点:-

• 如果您今天决定一项任务,那么完成这些任务的可能性很高。
• 如果一项任务是从昨天开始的,那么该任务可能已获得低优先级,并且可以进一步延迟。
• 如果一项任务是从前天开始的,那么该任务很可能永远无法完成。

GC 的想法与此类似,并有以下假设:-

• 如果对象是新的,那么对象的生命周期可能很短。
• 如果对象是旧的,那么它可能有一个长的生命周期。

因此,GC 支持三个代(第 0 代、第 1 代和第 2 代)。
 

第 0 代包含所有新创建的对象。当应用程序创建对象时,它们首先进入第 0 代。当第 0 代填满时,GC 需要运行以释放内存资源。所以 GC 开始构建图并消除应用程序中不再使用的任何对象。如果 GC 无法从第 0 代移除对象,它会将其提升到第 1 代。如果在接下来的迭代中,它无法从第 1 代移除对象,它会被提升到第 2 代。.NET 运行时支持的最大代是 2。

下面是运行 CLR profiler 时分代对象的示例显示。如果您是 CLR profiler 的新手,可以从 DOTNET1.aspx 中了解基本知识。
 

好的,分代是如何帮助优化的?
 

由于对象现在包含在代中,GC 可以选择要清理哪一代的对象。如果您还记得上一节,我们谈到了 GC 关于对象年龄的假设。GC 假设所有新对象都有较短的生命周期。所以换句话说,GC 主要会遍历第 0 代对象,而不是遍历所有代中的所有对象。

如果第 0 代的清理不足以提供足够的内存,它将接着清理第 1 代,依此类推。此算法在很大程度上提高了 GC 性能。
 

关于分代的结论
 

• Gen 1 和 Gen 2 中对象数量庞大意味着内存利用率不高。
• Gen 1 和 Gen 2 区域越大,GC 算法的性能越差。
 

使用 finalize/析构函数会导致 Gen 1 和 Gen 2 中对象增多
 

C# 编译器将析构函数翻译(重命名)为 Finalize。如果您使用 IDASM 查看 IL 代码,您会发现析构函数被重命名为 finalize。所以,让我们试着理解为什么实现析构函数会导致 Gen 1 和 Gen 2 区域中的对象增多。实际过程如下:-

• 当创建新对象时,它们会被移到 gen 0。
• 当 gen 0 填满时,GC 运行并尝试清理内存。
• 如果对象没有析构函数,如果它们未被使用,它会直接清理它们。
• 如果对象有一个 finalize 方法,它会将这些对象移到 finalization 队列。
• 如果对象是可访问的,则将其移到“Freachable”队列。如果对象不可访问,则回收内存。
• GC 本次迭代的工作完成。
• 下一次 GC 再次启动时,它会转到 Freachable 队列检查对象是否不可访问。如果对象不可从 Freachable 访问,则回收内存。
 

换句话说,带有析构函数的对象可以在内存中停留更长时间。

让我们尝试实际看一下。下面是一个简单的类,它有一个析构函数。
 

class clsMyClass 
{ 
public clsMyClass()
{ 
}
~clsMyClass()
{
}
}

 

我们将创建 100 * 10000 个对象,并使用 CLR profiler 进行监视。
 

for (int i = 0; i < 100 * 10000; i++)
{
clsMyClass obj = new clsMyClass();
}

 

如果您查看 CLR profiler 按地址的内存报告,您会看到 gen 1 中有很多对象。
 

现在,让我们删除析构函数并执行相同的操作。
 

class clsMyClass 
{ 
public clsMyClass()
{ 
}
}

 

您可以看到 gen 0 显著增加,而 gen 1 和 2 的数量则减少。
 

如果我们进行一对一比较,结果如下面的图所示。

通过使用 Dispose 来摆脱析构函数
 

我们可以通过在 dispose 方法中实现清理代码来摆脱析构函数。为此,我们需要实现“IDisposable”方法,在其中编写清理代码,并调用 suppress finalize 方法,如下面的代码片段所示。“SuppressFinalize”指示 GC 不要调用 finalize 方法。这样就不会发生双重 GC 调用。
 

class clsMyClass : IDisposable
{ 
public clsMyClass()
{ 
}
~clsMyClass()
{
}

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

 

现在客户端需要确保调用 dispose 方法,如下所示。
 

for (int i = 0; i < 100 ; i++)
{
clsMyClass obj = new clsMyClass();
obj.Dispose(); 
}

 

下面是使用析构函数和使用 dispose 时 Gen 0 和 Gen 1 分布情况的对比。您可以看到 gen 0 分配有了显著的改进,这表明内存分配良好。
 

如果开发者忘记调用 Dispose 怎么办?
 

这不是一个完美的世界。我们无法确保客户端始终调用 dispose 方法。这就是我们可以在接下来的章节中解释的 Finalize / Dispose 模式。

该模式的详细实现可在 http://msdn.microsoft.com/en-us/library/b1yfkh5e(VS.71).aspx 找到。

以下是 finalize / dispose 模式的实现方式。
 

class clsMyClass : IDisposable
{ 
public clsMyClass()
{

}

~clsMyClass()
{
// In case the client forgets to call
// Dispose , destructor will be invoked for
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Free managed objects.
}
// Free unmanaged objects

}

public void Dispose()
{
Dispose(true);
// Ensure that the destructor is not called
GC.SuppressFinalize(this);
} 
}

 

代码说明:-

• 我们定义了一个名为 Dispose 的方法,它接受一个布尔标志。此标志指示此方法是从 Dispose 还是从析构函数调用。如果它是从“Dispose”方法调用的,那么我们可以释放托管和非托管资源。
• 如果此方法是从析构函数调用的,那么我们只释放非托管资源。
• 在 dispose 方法中,我们已抑制 finalize 并调用了带有 true 的 dispose。
• 在析构函数中,我们调用了带有 false 值的 dispose 函数。换句话说,我们假设 GC 会处理托管资源,而让析构函数调用来清理非托管资源。
换句话说,如果客户端不调用 dispose 函数,析构函数将负责清理非托管资源。
 

结论
 

• 不要将空析构函数放在类中。
• 如果需要清理,请使用 finalize dispose 模式并调用“SupressFinalize”方法。
• 如果一个类暴露了 dispose 方法,请确保从您的客户端代码中调用它。
• 应用程序应该在 Gen 0 中分配比 Gen 1 和 Gen 2 更多的对象。Gen 1 和 Gen 2 中更多的对象是 GC 算法执行不佳的标志。
 

源代码
 

您可以在 此处 找到 dispose 模式的示例源代码。
 

其他实践

有关最佳实践第一部分,请点击 此处

有关最佳实践第三部分,请点击 此处

有关最佳实践第四部分,请点击 此处

有关最佳实践第五部分,请点击 此处

我的 FAQ 文章
 

我明白这可能不是讨论我的 FAQ 的合适文章。只是想为我撰写 FAQ 系列一年而自我祝贺一下。以下是所有 FAQ 的汇总链接:-


Silverlight FAQ:- https://codeproject.org.cn/KB/WPF/WPFSilverLight.aspx 

LINQ FAQ:- https://codeproject.org.cn/KB/linq/LINQNewbie.aspx 

WWF FAQ:- https://codeproject.org.cn/KB/WF/WWF.aspx

WCF FAQ:- WCF.aspx 

Sharepoint FAQ:- SharePoint.aspx 

Ajax FAQ:- AjaxQuickFAQ.aspx 

Architecture FAQ:- SoftArchInter1.aspx 

Localization and globalization:- LocalizationGlobalizPart1.aspx

Project management FAQ:- ProjectManagementFAQ.aspx
 

如需进一步阅读,请观看以下面试准备视频和分步视频系列。

 

© . All rights reserved.