CSharp 垃圾回收

🌴前言

网上关于 Garbage Collect 的文章已多如牛毛, 所以这里主要说一下我遇到的问题和 GC 使用方面的一些注意事项以及自己对垃圾回收中几个方法的理解.

[] 以下 GC 的含义均为 : Garbage Collect.

🍀.NET的 GC 机制

先说一下 .Net 上的 GC 机制:

  1. GC 并不是能自动释放所有的资源, 它只能自动释放托管资源 .

  2. GC 并不是实时回收内存的, 具体回收内存的时间由 GC 自身的算法控制.

🥝托管资源和非托管资源

这两个具体怎么定义的没去查, 只说一下 GC 对待他们的区别.

托管资源

.NET 可以自动释放托管资源并回收其内存, 不需要人工干预.

这句话的意思就是说当我们写程序时, 创建了一个托管资源, 我们使用了一段时间后就不用了, 此时我们并不需要告诉程序: "这个资源已经用完了, 一会有空了的时候帮我回收一下这些内存, thank you!", .NET 会自动判断其是否已经不再使用, 如果 .NET 判断其确实已经不再使用了, 便会自动将其占用的内存回收.

非托管资源

.NET 不会自动回收非托管资源, 如需回收, 需要提前通知.

常见的非托管资源: 文件, 字体, 窗口, 网络连接, 数据库连接, 画刷, 图标等.

上面那句话的意思就是说当我们写程序时, 创建了一个非托管资源, 我们使用了一段时间后就不用了, 此时我们就必须告诉程序: "这个我用完了, 有空了一定要记得回收一下这块内存哈! thanks!". 不然的话, 那个非托管资源就会一直被我们的程序占用. 即使每过一段时间 .NET 都会来内存处收垃圾, 但是 .NET 永远也不知道这个资源已经成为垃圾了, 因为我们并没有告诉他.

这就是 .NET 对待两种资源的态度区别.

🌼我们如何告诉 .NET 非托管资源已经用完了呢?

想要给非托管资源打上一个 "可被回收" 标记, 需要使用 Dispose 方法.

显式调用 Dispose() 方法

对于实现了 IDisposable 接口的非托管资源, 可以直接调用其中的 Dispose() 方法, 这个方法可以用来告知程序: "这个资源已经用完了, 你抽空安排一下吧!" 😃

使用 using 语句块隐式调用 Dispose() 方法

所有实现了 IDisposable 接口的资源都可以放到 using 语句块中进行资源管理, 在 using 中进行声明以及实例化. 以下摘录自 Microsoft Document 中对 using 的介绍.

  1. IDisposable 对象的生存期限于单个方法时, 应在 using 语句中声明并实例化它.

  2. using 语句会按照正确的方式调用对象上的 Dispose 方法, 即使 using 语句块中出现了异常, 也能保证 Dispose 被正常调用.

  3. 在 using 块中, 对象是只读的并且无法进行修改或重新分配.

  4. 不要先实例化资源对象, 然后将变量传递到 using 语句, 而是应该直接在 using 语句中实例化该对象, 并将其范围限制在 using 块中.

🦄回收内存的非实时性

GC 一个很大的特点就是内存的回收并不是实时的, 它内部有一套完整的算法会进行智能判断回收的时机. 而且文章上面所提到的各种通知系统资源已使用完毕的方式也仅仅只是告诉系统这个 "非托管资源" 已经成为垃圾了, 可以被回收了. 但是实际上此时这块内存还没有被回收, 具体什么时候回收是由系统决定的.

👀GC.Collect() 方法

因此系统提供了一个 GC.Collect() 方法, 这个方法会以系统的 root 为基础层层遍历, 将所有的可回收内存全部回收. 借由此方法, 程序员可以立即回收内存. 但是除非特殊情况, 不要主动调用此方法, 频繁调用会严重影响程序性能. (微软说的~)

🙄我的问题

当时我遇到的问题是, ASP 程序中需要将数据库中的 240 万条数据导出到一个文件中, 大概 500MB 左右, 而程序是一次性将这全部的 240 万行数据读取出来, 放到一个临时的 DataSet 中, 之后向文件中写入. 但是这个 DataSet 过大, 直接导致内存溢出了......

于是我开始分批次读取, 并且使用 using 语句块进行资源的自动管理, 如下, 外面套了一层 for 循环.

1
2
3
4
5
6
7
8
// 追加模式写入流, 使用 using 自动管理资源
using (StreamWriter sw = new StreamWriter(path, true, Encoding.GetEncoding("GB2312")))
{
using (DataSet ds = new GOOGOSOFT.DATABASE.OracleHelper().GetDataSet(str_sql))
{
WriteFileCSV(ds, i, sw);
}
}

我一共分了 8 个批次查询, 但是当程序循环到第 6 次时, 还是内存溢出了......我当时就很郁闷, 不是都已经使用 using 了吗, 为啥还内存溢出......后来才知道, using 只是会告诉程序资源使用完毕了, 但并不会立即回收那部分内存. 于是在每次开始新一轮循环的时候强制回收一次内存就可以了.

1
2
3
4
5
6
7
8
9
10
11
// 追加模式写入流, 使用 using 自动释放资源
using (StreamWriter sw = new StreamWriter(path, true, Encoding.GetEncoding("GB2312")))
{
using (DataSet ds = new GOOGOSOFT.DATABASE.OracleHelper().GetDataSet(str_sql))
{
WriteFileCSV(ds, i, sw);
}
}

// 强制调用垃圾回收器, 回收上面资源占用的内存
GC.Collect();

我觉得释放和回收可以这样理解:

  1. 释放是指解除对非托管资源的占用和锁定.

比如一个文件, 在没有释放资源的时候, 不能对其进行其他操作, 比如删除操作. 释放资源后可以进行删除.

  1. 回收是指将资源使用的内存进行回收.

比如我之前遇到的问题, 释放资源只是将资源打上一个 "可被回收" 的标记等待被 GC 回收, 此时内存还是被占用的, 之后 GC 真正回收内存之后, 内存使用率才会真正降低...

最后还是要在强调一下, 通常情况下, 我们应该避免调用 GC.Collect() 方法, 让垃圾回收器独立运行. 在大多数情况下, 对于执行回收的最佳时机, 垃圾回收器的算法更有优势.

除非在某些特殊情况下, 我们的程序占用了大量的内存, 需要立即释放, 在这种情况下我们才能使用 GC.Collect() 方法手动回收内存.

🐬参考文章

  • 从 C# 垃圾回收机制中挖掘性能优化方案

  • using 语句

  • 关于using和System.GC.Collect()对于释放资源的讨论

  • .Net中Finalize()和Dispose()有什么区别?