垃圾收集和使用 - 为什么在 `using{}` 块后没有释放内存?

Garbage Collection and Using - Why is memory not released after `using{}` block?

我最近一直在重构一些旧的数据库访问代码。我有一个包含数百种方法的库,看起来像这样

public int getFoo(int id)
{
    using(SqlConnection connection = ConnectionManager.GetConnection())
    {
        string sql = "SELECT TOP(1) foo FROM bar WHERE id=@id";

        SqlCommand command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@id", id);
        return (int)command.ExecuteScalar();
    }
}

我认为将 SqlCommand 包装到 using{} 块中(就像 SqlConnection 已经是这样)是一个明智的做法,以便尽快处理资源可能的。出于求知欲,我决定制作以下小控制台应用程序以查看将释放多少内存:

using (SqlConnection conn = ConnectionManager.GetConnection())
{
    WeakReference reference;
    string sql = "SELECT COUNT(foo) FROM bar";
    Console.WriteLine("Memory Allocated before SqlCommand: " + GC.GetTotalMemory(true));
    using (SqlCommand comm = new SqlCommand(sql,conn))
    {
        Console.WriteLine("Memory Allocated after SqlCommand: " + GC.GetTotalMemory(true));
        reference = new WeakReference(comm);

        Console.WriteLine("SQL output: " + comm.ExecuteScalar());

        Console.WriteLine(
            "Memory Allocated before dispose SqlCommand: " + GC.GetTotalMemory(true));
    }
    GC.Collect();
    Console.WriteLine("Memory Allocated after SqlCommand: " + GC.GetTotalMemory(true));
    Console.WriteLine("Reference is alive: " + reference.IsAlive);

    Console.ReadLine();
}

令我惊讶的是,这是我得到的输出:

Memory Allocated before SqlCommand: 236384

Memory Allocated after SqlCommand: 239160

SQL output: (whatever)

Memory Allocated before dispose SqlCommand: 246416

Memory Allocated after dispose SqlCommand: 246548 <-- It's gone up!?

Reference is alive: True <-- Why is reference still alive?

起初我虽然我的 WeakReference 可能以某种方式让 command 存活,但我注释掉了该代码,但我仍然得到了类似的结果。

为什么 command 在这里没有被垃圾回收,即使 GC.Collect() 已被显式调用?如果在 using 块中引入了一个变量,我们什么时候可以期望该变量符合垃圾回收条件?

处理对象与垃圾收集没有任何关系。垃圾收集与清理 托管 资源有关。处置是为了清理 GC 未跟踪 非托管 资源。

有时这些非托管资源是在不通过 GC 的情况下显式分配的内存,有时它们锁定在文件句柄、数据库连接、网络套接字或任何数量的其他可能性上。但无论它们是什么,它们非常明确地 不会 是 GC 正在跟踪的内存,这就是你正在测量的内存。

就差异而言,您的差异只是在程序的噪音水平之内。您的更改不会对使用多少托管内存产生有意义的影响,您看到的差异与使用垃圾回收的程序中内存的正常波动一致。

在处理 SqlConnection 后,如果它是该程序中使用的第一个连接,我预计内存使用会略有增加。

SqlConnection.Dispose()SqlConnection.Close()会默认将内部用来管理连接的class存储在一个静态线程安全的集合中,这样下一个SqlConnection.Open()就可以使用该对象,而不是通过建立另一个数据库连接的过程。

线程安全集合将占用它自己的一些内存,即使它已经存在,处理对连接的引用的内部对象也会导致内部向上调整大小。

因此我希望这里的尺寸会略有增加。

If a variable is introduced in a using block when can we expect that variable to be eligible for garbage collection?

我们肯定不会!

想一想GC收集的作用。

它做的第一件事是标记它可以不能收集的所有对象。

  1. 如果对象由 static 引用持有,则无法收集。

  2. 如果某个对象被活动线程堆栈之一上的变量引用,则无法收集。

  3. 如果一个对象被对象中的某个字段引用到无法采集,那么就无法采集。

现在,当您执行 SqlCommand comm = new SqlCommand(sql,conn) 时,当前线程堆栈上的一个字被设置为指向构造函数创建的对象。

每次使用 comm.

时,抖动产生的机器代码会使用堆栈上的那个位置。

根据实现情况,该指针的其他副本可能会也可能不会被放入堆栈。

在代码中最后一次使用该对象后(using 块末尾导致的隐藏隐式 comm.Dispose()),那么堆栈中的那些词 可以 被重用。

他们可能不是。如果您在调试模式下编译,它们肯定不会,因为它会混淆调试,让变量在它们仍在范围内时突然消失。

如果方法的其余部分没有任何代码在堆栈上使用相同的 space,而您没有,则它们不太可能是。例如,如果您在 using 结束之后和 GC.Collect() 之前添加了一个 object whatever = new object(),那么您更有可能发现 reference.IsAlive 是错误的,因为抖动 可能 使用对 whatever 的新引用的那部分堆栈内存(特别是如果在 GC.Collect() 之后使用了 whatever 以便它不只是完全优化掉了)。

using是调用Dispose(),而Dispose()的非常是与托管内存无关;如果是这样,那么我们就不需要它了,因为 GC 会为我们处理。

当对象可以显示为可收集时,它们就会被收集;这 可以 在最后一次使用它们的引用之后的任何时间发生,但是 可能 在最后一次使用它们的方法之后才会发生returns,直到那时在堆栈中引用它们的内存片段可能仍会引用它们的位置。