第12章 对象销毁与垃圾回收
销毁(disposal),指文件、锁、操作系统句柄和非托管对象的释放,相应的功能由 IDisposable 提供;垃圾回收,指托管内存不再使用后的定期回收,由 CLR 执行。
销毁通常是显示调用的;垃圾回收是自动执行的。
12.1 IDisposable
接口、Dispose
方法和 Close
方法
.NET 提供了 IDisposable
接口,用于 using
语句释放资源。接口定义如下:
public interface IDisposable
{
void Dispose();
}
using
语句实际将代码包裹在 try/finally 中,如下两段代码等价:
using (FileStream fs = new FileStream("myFile.txt", FileMode.Open))
{
// ... 写入文件 ...
}
FileStream fs = new FileStream("myFile.txt", FileMode.Open);
try
{
// ... 写入文件 ...
}
finally
{
((IDisposable)fs)?.Dispose();
}
Warn
using + Dispose 模式可以保证抛出异常也会释放资源,不过也有例外,见22.9.1 Abort 的使用陷阱
12.1.1 标准销毁语义
.NET 的销毁遵循如下逻辑:
- 对象一旦销毁就无法再恢复,也不能够重新激活。在销毁之后继续调用其方法(
Dispose
除外)或访问其属性都将抛出 ObjectDisposedException
。 - 可以重复调用对象的
Dispose
方法,且不会发生任何错误。 - 若可销毁对象
x
“拥有”可销毁对象y
,则 x
的Dispose
方法需自动调用 y
的Dispose
方法,接到其他指令的情况除外。
12.1.1.1 Close
方法
除了 Dispose
方法,一些类型还定义了 Close
方法。多数 Close
都满足以下两者中的一个:
- 从功能上等价于
Dispose
方法 - 从功能上是
Dispose
方法的子集
以 IDbConnection
接口为例:Close
的连接可以重新打开,Dispose
的连接则不能;
以 Windows 窗体为例, ShowDialog
激活的窗体:Close
方法隐藏窗体,Dispose
方法释放它的资源。
12.1.1.2 Stop
方法
一些类定义了 Stop
方法,Stop
方法可能会释放非托管资源,与 Dispose
不同的是,它允许调用 Start
方法重新开始。
例如 Timer
和 HttpListener
。
12.1.2 销毁对象的时机
包含 非托管资源句柄 的对象几乎都需要销毁代码来释放句柄。如:Windows 窗体控件、文件或网络流、网络套接字、GDI+ 的笔触(Pen)、笔刷(Brush)和位图对象。
一般来说这些非托管资源不进行销毁可能造成意外的麻烦。不过有三种情况不适用于销毁对象:
1. 使用者并不 持有 该对象
该情况很少见,主要案例是 System.Drawing 中预定义的静态字段/属性(如 Brushes.Blue),它在整个应用程序声明周期中都可能会用到。
2. 该对象的 Dispose()
方法执行了 期待之外的 操作
该情况最常见,以 System.IO 和 System.Data 中的数据类型为例,有:
类型 | 销毁功能 | 何时不需要销毁 |
---|---|---|
MemoryStream |
防止进一步的输入和输出 | 后续操作仍然需要读写这个流 |
StreamReader 、StreamWriter |
清空读取器和写入器,并关闭底层流 | 当需要保持相关的流的打开状态时(相应的,必须在完成操作之后立即调用 StreamWriter 的 Flush() 方法) |
IDbConnection |
释放数据库连接,清空连接字符串 | 如果需要重新打开数据库连接,则应当调用 Close() 而不是 Dispose() 方法 |
DataContext (LINQ to SQL) |
防止进一步使用 | 当后续的延迟执行的查询仍然需要连接上下文时 |
3. 该对象的 Dispose()
方法在设计上不是必须,且释放该对象会增加程序 复杂度
此类情况主要涉及如下类型:
-
WebClient
-
StringReader
、StringWriter
-
BackgroundWorker
位于 System.ComponentModel 命名空间下
这些类型由于 内部实现借助了可销毁类型 因此实现了 Dispose 模式,但不意味着它们需要进行销毁。如果类型实例需要长时间使用,可以忽略对象的销毁。
12.1.3 选择性销毁
有时候我们想选择性销毁一些内容(例如 StringReader
释放时底层流不被释放)。此时可以使用“ 选择性销毁 模式”实现这一效果。
我们在不释放底层流的适配器有提到:
Summary
包括内存数据压缩中提到的
DeflateStream
,有三个流支持 Close 后底层流仍保持打开状态
这三个流恰好实现了该模式(以 DeflateStream
为例):
public DeflateStream(Stream stream, CompressionLevel compressionLevel, bool leaveOpen) { ... }
protected override void Dispose(bool disposing)
{
try
{
PurgeBuffers(disposing);
}
finally
{
try
{
if (disposing && !_leaveOpen)
{
Stream stream = _stream;
if (stream != null)
{
stream.Dispose();
}
}
}
finally
{
...
}
}
}
12.1.4 在销毁时清理字段
本书建议在 Dispose()
方法中做这些事:
-
取消 相关事件 的订阅
-
释放 非托管 内存
注意:
Dispose()
方法并没有释放托管内存,托管内存只有 GC 时才会释放。 -
释放 保密 数据
例如
SymmetricAlgorithm
类在销毁时会调用Array.Clear()
清除持有的加密密钥。 -
标记对象已释放
这样用于调用对象时可以抛出
ObjectDisposedException
异常。
DO
:如果任何成员在 Dispose 后无法继续使用,则被调用时要抛出 ObjectDisposedException
异常。
12.2 自动垃圾回收
垃圾回收是周期进行的,CLR 会基于如下因素决定何时回收:
- 可用的 内存
- 已分配的 内存
- 最后一次回收的 间隔
对象在不被引用后,其实际释放时间并不确定,它可能在几纳秒后释放,也可能在几天后。
12.2.1 根
根可以使对象保持 存活 。如果对象没有直接或者间接地被根引用,那么它就可以被 GC 。
根有如下几种:
- 当前正在执行的 方法 (或在其调用栈的任何一个方法中)的局部变量或者参数
- 静态 变量
- 终结队列中的对象(见12.3 终结器)
相互循环引用的对象组不在上述范围内。具体的关系见下图:
C7.0 核心技术指南 第7版.pdf - p558 - C7.0 核心技术指南 第7版-P558-20241113180058-ai6x08j
12.2.2 垃圾回收和 WinRT
#suspend#WinRT 开发尚未接触,因此这里我不清楚是做什么的。该内容在《C#12 核心即使指南》中已移除。
Windows Runtime 依赖 COM 的引用计数机制,而不是自动化的垃圾回收器,来释放内存。即便如此,从 C# 实例化的 WinRT 对象的生命周期也是靠 CLR 的垃圾回收器管理的。这是因为 CLR 会在背后创建一个名为运行时可调用包装器(请参见第24章)的对象,而它将通过这个中间对象访问 COM 对象。
12.3 终结器
12.3.0 终结器的定义
12.3.0.1 终结器的声明
终结器和构造器的声明方式相似,但它以 ~ 作为前缀:
class Test
{
˜Test()
{
// Finalizer logic...
}
}
它有如下特点:
- 无法声明为 public 或 static
- 无法拥有 参数
- 无法调用 基类
12.3.0.2 终结器的作业方式
终结器和 GC 是分不同阶段进行的:
-
GC 确定未使用的可删除的对象,没有 终结器 的对象会被直接删除,有 终结器 的对象会保持存活,并放入一个特殊的队列;
至此,GC 完成
-
终结器线程与应用程序并行执行,取出特殊队列中的对象并执行其 终结 方法;
在终结器执行之前,对象仍是存活的,这个特殊队列扮演着 根 的角色
-
对象终结器执行完毕,对象将成为 未引用 对象,等待下一次 GC 时删除
12.3.0.3 使用终结器的代价
-
终结器会降低 内存 分配和回收的速度;
GC需要对终结器的执行进行追踪。
-
终结器延长了对象和该对象所引用的对象的 生命周期 ;
它们必须等到下一次垃圾回收时才会被真正删除。
-
无法预测多个对象的终结器调用的顺序;
-
开发者对于终结器调用的时机只有非常有限的控制能力;
-
如果一个终结器的代码阻塞,则其他对象 也无法终结 ;
-
如果应用程序没有被完全 卸载 ,则对象的终结器也可能无法得以执行。
总之,虽然终结器的存在非常必要,但是除非绝对必要,通常都不会希望使用它。如果要使用它,需要 100% 理解它所做的一切。
12.3.0.4 终结器实现准则
- 保证终结器可以很快执行完毕
- 永远 不要 阻塞终结器的执行;
- 不要 引用 其他可终结对象;
- 不要 在终结器中抛出异常。
Notice
对象的终结器甚至可以在 对象构造器 抛出异常时调用。因此要注意:在编写终结器时,对象的字段有可能并没有初始化完毕。
12.3.1 在终结器中调用 Dispose()
Info
更多内容见9.4 Dispose 模式
在终结器中调用 Dispose()
方法是一个常见的模式,该模式更像是一个优化行为,而不是必要行为。
Notice
需要注意,在这种模式下,内存的回收和资源的回收两件事情耦合在了一起,而实际上它们的关注点是不同的(除非资源本身就是内存)。此外,这种模式会增加终结线程的 负担 。
这个模式通常作为消费者忘记调用
Dispose()
方法的补救措施。
该模式的标准实现如下:
class Test : IDisposable
{
public void Dispose() // NOT virtual
{
Dispose (true);
GC.SuppressFinalize (this); // Prevent finalizer from running.
}
protected virtual void Dispose (bool disposing)
{
if (disposing)
{
// Call Dispose() on other objects owned by this instance.
// You can reference other finalizable objects here.
// ...
}
// Release unmanaged resources owned by (just) this object.
// ...
}
~Test() => Dispose (false);
}
disposing
标志用于区分 Dispose()
方法是由用户调用的还是由终结器进行调用的(最后的补救)。终结器调用时(即 disposing
为 false ),该方法不可再引用其他可终结对象(其他对象可能因为终结已处于未知的状态)。在 disposing
为 false 时,我们可能还会执行这些工作:
- 释放任何直接引用的 操作系统 资源(例如,通过 P/Invoke 调用 Win32 API 获得的资源)。
- 删除构造过程中创建的 临时 文件。
该模式还有如下建议:
-
方法中的代码都应该包裹在 try/catch 块中,并在异常出现时记录日志;
要保证该方法健壮,日志本身也应当尽可能简单、健壮。
-
无参
Dispose()
方法需调用 GC.SuppressFinalize()
方法该方法用于防止 GC 在之后回收时再次执行终结器,用于提高性能(可以做到在一个周期之内将对象(及其引用)回收)。该方法的调用不是必须的(因为
Dispose()
方法能够重复调用)。
12.3.2 对象的复活
“复活”,指:因为终结器运行失败或其他理由,开发者希望原对象仍 被保留 ,而非 被 GC 回收 。这种高级处理方式被称为“复活(resurrection)”
以如下代码为例,File.Delete()
可能因诸多原因执行失败导致终结器异常。我们希望后续可以获取失败原因,此时便可以使用该处理方式:
public class TempFileRef
{
internal static readonly ConcurrentQueue<TempFileRef> FailedDeletions = new ConcurrentQueue<TempFileRef>();
public readonly string FilePath;
public Exception DeletionError { get; private set; }
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch (Exception ex)
{
DeletionError = ex;
FailedDeletions.Enqueue (this); // Resurrection
}
}
}
GC.ReRegisterForFinalize()
方法
复活的对象其终结器不会重新执行。若要重新执行终结器,需调用 GC.ReRegisterForFinalize()
方法,重新注册对象,在下次 GC 时重试。
以如下代码为例,我们可以结合“复活”,在第三次执行失败时进行复活:
public class TempFileRef
{
internal static readonly ConcurrentQueue<TempFileRef> FailedDeletions = new ConcurrentQueue<TempFileRef>();
public readonly string FilePath;
int _deleteAttempt;
public Exception DeletionError { get; private set; }
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch (Exception ex)
{
if (_deleteAttempt++ < 3)
{
GC.ReRegisterForFinalize (this);
}
else
{
DeletionError = ex;
FailedDeletions.Enqueue (this); // Resurrection
}
}
}
}
Warn
切勿在终结器的一次执行中多次执行该方法。如果执行两次,对象将会注册两次并进行两次终结。
12.4 垃圾回收器的工作方式
-
内存分配时判断是否需要 GC;
内存分配通过 new 关键字触发,也可以通过
GC.Collect()
方法手动触发。 -
触发 GC,此时可能冻结所有线程(见12.4.1.3 并发和后台回收);
-
GC 从根对象开始遍历对象图,将遍历到的对象标记为 可达 。未被标记的对象将进行回收;
-
判断未引用对象是否包含终结器;
不包含则立即回收,包含则放入 终结器队列 ,在 GC 完成后交由终结器线程处理,并在下次 GC 时进行回收;
-
压缩内存
剩余的存活对象对象将移动到堆的起始位置(压缩),释放出更多的对象空间来容纳更多的对象。
Tips
这种压缩过程的目的有两个:
- 防止内存碎片化;
- GC 可以用很简单的策略来分配新的对象:将新的对象分配在堆的尾部即可。
此外它还避免了耗时的内存片段列表的维护开销。
GC 的工作方式如下流程图演示:
flowchart LR node1(["开始"]) node2["内存分配"] node3{"内存分配是否超量<br>是否需要降低内存用量"} node4["触发 GC"] node5["冻结所有线程"] node6["遍历对象图<br>(根据对象引用)"] node7{"是否包含终结器"} node8["进行回收"] node9["置于终结器队列<br>等待下一次 GC"] node10[("有引用对象")] node11[(未引用对象)] node12["内存压缩"] node13(["结束"]) node6-->node10--->node12-->node13 node1-->node2-->node3--"是"-->node4-->node5-->node6-->node11-->node7--"否"-->node8-->node13 node7--"是"-->node9-->node2 node3--"否"-->node212.4.1 优化技术
GC 使用了多种优化技术来减少垃圾回收的时间。
12.4.1.1 分代回收
分代回收是最重要的优化措施,减少了对长时间存活对象的追踪、回收。GC 将堆上的内存分为 三 代,其中第 0 代和第 1 代称为“短生存期(ephemeral)”的代:
划分标准 | 回收频率 | 可用空间(以 64 位工作站 CLR 为基准) | 用时(粗略估计) | |
---|---|---|---|---|
0 代 | 刚刚分配的对象 | 高 | 最大 256MB,通常仅有几百 KB 至几 MB | 不到 1 ms |
1 代 | 第 一 轮 GC 中存活的对象 | 较高 | 与 1 代相似,作为 2 代的缓冲区 | |
2 代 | 其他所有对象 | 低 | 无限制 | 可能需要 100 ms |
一次完整的 GC 包含 2 代内存的回收。完整回收的效果如下:
C7.0 核心技术指南 第7版.pdf - p564 - C7.0 核心技术指南 第7版-P564-20241115232144-jnbq0jg
通过分代回收,存活周期短的对象可以非常有效的回收。以如下代码为例,StringBuilder
对象几乎一定会在第 0 代被快速回收:
string Foo()
{
var sb1 = new StringBuilder ("test");
sb1.Append ("...");
var sb2 = new StringBuilder ("test");
sb2.Append (sb1.ToString());
return sb2.ToString();
}
12.4.1.2 大对象堆
当某个对象占用的内存超过限定的阈值(目前是 85000 byte(截至 C#12 仍是该值)),该对象会被存放在 独立的堆 中。该堆被称为“大对象堆(Large Object Heap, LOH)”。
大对象堆有如下特点:
-
避免过量的第 0 代回收
如果没有 LOH,几乎每次分配 16MB 对象都会触发 0 代 GC
-
不会被 压缩
移动大块内存的开销总是很大
因为不被压缩,大对象堆也有这些问题:
-
大对象的内存 分配 较为缓慢
GC 无法简单的在堆尾分配对象,它需要关心中间的空隙(即需要维护空闲内存块链表)
-
大对象堆有可能 碎片 化
碎片化的内存会在堆上产生一个空洞(毕竟一个对象有 85000 byte 大小),而这个空洞很难有合适的对象填补
上述现象如果造成了问题,也可以控制 GC 在下一次回收时压缩大对象堆,这些对象都会按第 2 代处理:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
12.4.1.3 并发和后台回收
GC 的第 0、1 代回收会将执行线程冻结,对于第 2 代回收则有不同:
- 桌面端应用:因回收时间较长,GC 允许 线程运行;
- 服务器 CLR:因服务器没有用户界面, 阻塞 回收不是大问题,仍采用 阻塞 回收。
从 CLR 4.0 开始,第 2 代回收进行了优化,优化后的“并发回收”改名为“后台回收”,它解除了若干限制。例如:在第 2 代回收时,若触发了 0 代回收,并发回收会终止,而优化后的后台回收会继续执行。
Tips
对于服务器来说,延迟不是最大的问题,吞吐量才是。所以服务器 CLR 会调动所有可用核心进行 GC,一时的阻塞不会影响整体的吞吐量。
12.4.1.4 垃圾回收通知(服务器 CLR)
服务器版本的 CLR 可以在进行完全 GC 前发送通知,借此可将回收之前的请求转移至其他服务器,用于优化响应时间。
启用通知的步骤如下:
-
调用
GC.RegisterForFullGCNotification()
方法; -
开启另一个线程,并调用
GC.WaitForFullGCApproach()
方法,等待 返回 ;当该方法返回
GCNotificationStatus
时表示 回收做好了准备 -
将请求路由至其他服务器后,调用
GC.Collect()
方法强制手动触发一次回收; -
调用
GC.WaitForFullGCComplete()
方法等待 回收完成 。回收完成,此时可以重新开始接受请求
12.4.2 强制垃圾回收
GC.Collect()
方法用于强制垃圾回收,它接受一个 int 值表示 回收代数 ,若不传参,将进行完整回收。
一般来说我们很少会强制垃圾回收,由 GC 自主决定会得到最佳性能。强制垃圾回收会有如下缺点:
-
令第 0 代(第 1 代)对象不必要的提升至第 1 代(第 2 代)对象中;
-
影响 GC 自我调整 能力
垃圾回收器可以动态调整每一代回收阈值,强制垃圾回收会破坏这种动态调整
强制回收一般用于如下场景:
-
应用程序试图 休眠一段时间
例如每 24 小时仅执行一次的任务(常见的是 Windows 服务),由于没有新的内存分配,GC 不会 自动触发 ,消耗的内存会在剩下的 24 小时内保留
-
测试 某个类的终结器
针对第一个场景,为避免终结器的存在导致的内存未完全释放,可以额外调用 GC.WaitForPendingFinalizers()
等待终结器执行完成后再次强制回收:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Tips
以上方式经常在一个循环中执行:通常运行对象的终结器会释放更多同样拥有终结器的对象。
12.4.3 垃圾回收过程的调整
垃圾回收有若干属性、方法可以调整回收频率、方式:
-
GCSettings.LatencyMode
属性:可以在 延迟 和 效率 上进行权衡默认值为
Interactive
,改为LowLaatency
将使 CLR 执行更快速的回收(但是频率也更高),适用于需要快速响应的程序; -
GC.TryStartNoGCRegion()
和GC.TryStartNoGCRegion()
方法:可以 挂起 、 恢复 垃圾回收
12.4.4 内存压力
机器的总内存负载,是影响运行时释放回收的重要因素。对于非托管内存,运行时无法进行评估(CLR 仅了解 托管 内存),因此会误以为内存负载较低。
此时我们可以调用 GC.AddMemoryPressure()
方法传入 占用的非托管内存 大小,当非托管内存释放时,调用 GC.RemoveMemoryPressure()
方法 释放该内存压力 。通过这两个方法,可以让运行时重新评估是否要释放托管内存,以降低内存的整体占用。
12.5 托管内存泄漏
在托管语言中,由于 CLR 的自动垃圾回收系统,很少会出现内存泄漏的错误。
尽管如此,大型复杂的 .NET 应用程序仍有可能遇到同样的问题:某些对象虽然不再使用,但其引用被“ 根 ”进行了保留。
最常见的一种情况是 事件 ,它保存着目标对象的引用(见4.1.3 实例目标方法和静态目标方法)。以如下代码为例,静态实例 Test._host
的 Click
事件实际持有着所有 Client
实例,导致内存无法释放:
class Test
{
static Host _host = new Host();
public static void CreateClients()
{
Client[] clients = Enumerable.Range (0, 1000)
.Select (i => new Client (_host))
.ToArray();
// Do something with clients ...
}
}
class Host
{
public event EventHandler Click;
}
class Client
{
Host _host;
public Client (Host host)
{
_host = host;
_host.Click += HostClicked;
}
void HostClicked (object sender, EventArgs e) { ... }
}
解决方法之一是令 Client
实现 IDisposable
接口,并在 Dispose()
方法中 注销事件处理器 :
public void Dispose() { _host.Click -= HostClicked; }
而 Client
的消费者应当在 使用完毕 后销毁这些实例:
Array.ForEach (clients, c => c.Dispose());
12.5.1 定时器
定时器是另一个容易造成内存泄漏的因素,.NET Framework 会确保定时器的存活,而定时器的 Elapsed
事件 又会保证对象实例(事件处理器的拥有者)存活。在22.11 定时器我们会了解到 4 种定时器,其中 3 种都有该问题(System.Threading.Timer
除外)。
以如下代码为例,Foo
实例将永远不会回收。
using System.Timers;
class Foo
{
Timer _timer;
Foo()
{
_timer = new System.Timers.Timer { Interval = 1000 };
_timer.Elapsed += tmr_Elapsed;
_timer.Start();
}
void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... }
}
解决的方法也很简单,令 Foo
实现 IDisposable
接口,并在实现中调用定时器的 IDisposable.Dispose()
方法即可:
class Foo : IDisposable
{
...
public void Dispose() { _timer.Dispose(); }
}
System.Threading.Timer
定时器则不同,它通过委托而非事件完成方法的调用,即使忘记手动 Dispose,也会触发终结器将其销毁。
Suggest
一个很好的准则是:如果类中任何字段所赋的对象实现了
IDisposable
接口,那么该类也应当实现 IDisposable
接口。
12.5.2 诊断内存泄漏
最简单的避免托管内存泄露的方式就是在编写应用程序时主动监视内存的使用状况。我们可以用以下语句获得当前程序中对象的内存消耗(true 参数告知 GC 首先执行一次垃圾回收):
long memoryUsed = GC.GetTotalMemory (true);
对于测试驱动的开发,可以利用单元测试判断内存是否按照预想进行了回收。
如果应用程序已发生托管内存泄漏,可以使用的工具有:
- windbg.exe
- CLR Profiler(Microsoft)
- Memory Profiler(SciTech)
- ANTS Memory Profiler(Red Gate)
- Windows WMI 计数器
12.6 弱引用
弱引用可以让开发者保持对象的引用,同时允许 GC 在必要时 释放对象,回收内存 。
一般使用场景:对象过 大 ,并且 不经常访问 。这样我们就可以创建一个弱引用,当不常用该对象的时候,GC 可以回收该对象,当需要引用对象,可以先判断弱引用的对象是不是存在,如果存在,就直接使用,如果弱引用的对象已经被回收,那就重新创建一个对象来使用。
弱引用通过 System.WeakReference
类操作,用法如下:
var sb = new StringBuilder ("this is a test");
var weak = new WeakReference (sb);
// 此处为防止使用时目标被回收,先将目标赋值给局部变量
var sb = (StringBuilder) weak.Target;
if (sb != null)
{
/* Do something with sb */
Console.WriteLine (weak.Target); // This is a test
}
下面是一个较为完整的用例,Widget
使用弱引用来追踪实例化的 Widget
对象,但不阻止这些对象的回收:
class Widget
{
static List<WeakReference> _allWidgets = new List<WeakReference>();
public readonly string Name;
public Widget (string name)
{
Name = name;
_allWidgets.Add (new WeakReference (this));
}
public static void ListAllWidgets()
{
foreach (WeakReference weak in _allWidgets)
{
Widget w = (Widget)weak.Target;
if (w != null) Console.WriteLine (w.Name);
}
}
}
12.6.1 弱引用和缓存
WeakReference
的用途之一是缓存大的对象图,这使得我们可以简单地缓存占用内存较多的数据而不会引起内存过度消耗:
_weakCache = new WeakReference (...); // _weakCache is a field
...
var cache = _weakCache.Target;
if (cache == null) { /* Re-create cache & assign it to _weakCache */ }
上述策略并不实用,因为我们无法控制垃圾回收何时触发,缓存的数据可能在几毫秒内就被回收了。
一般来说,至少需要两个级别的缓存:一开始使用 强引用 进行缓存,在一定时间后再转换为 弱引用 。
12.6.2 弱引用和事件
如下代码自定义了一套“事件”,它通过弱引用来保存所有的委托,而非用委托自身的“ 多播 ”特性,以解决事件未取消订阅导致的内存泄漏:
public class WeakDelegate<TDelegate> where TDelegate : Delegate
{
class MethodTarget
{
public readonly WeakReference Reference;
public readonly MethodInfo Method;
public MethodTarget(Delegate d)
{
// d.Target will be null for static method targets:
if (d.Target != null) Reference = new WeakReference(d.Target);
Method = d.Method;
}
}
List<MethodTarget> _targets = new List<MethodTarget>();
public void Combine(TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
_targets.Add(new MethodTarget(d));
}
public void Remove(TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
{
MethodTarget mt = _targets.Find(w =>
Equals(d.Target, w.Reference?.Target) &&
Equals(d.Method.MethodHandle, w.Method.MethodHandle));
if (mt != null) _targets.Remove(mt);
}
}
public TDelegate Target
{
get
{
Delegate combinedTarget = null;
foreach (MethodTarget mt in _targets.ToArray())
{
WeakReference wr = mt.Reference;
// Static target || alive instance target
if (wr == null || wr.Target != null)
{
var newDelegate = Delegate.CreateDelegate(
typeof(TDelegate), wr?.Target, mt.Method);
combinedTarget = Delegate.Combine(combinedTarget, newDelegate);
}
else
_targets.Remove(mt);
}
return combinedTarget as TDelegate;
}
set
{
_targets.Clear();
Combine(value);
}
}
}
它的使用方式如下:
public class Foo
{
WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>();
public event EventHandler Click
{
add { _click.Combine(value); }
remove { _click.Remove(value); }
}
protected virtual void OnClick(EventArgs e)
=> _click.Target?.Invoke(this, e);
}
Tips
老实说我觉得用起来有点繁琐了。
标签:12,对象,终结,Dispose,回收,销毁,GC,垃圾,内存 From: https://www.cnblogs.com/hihaojie/p/18646139/chapter-12-object-destruction-and-garbage-recy