【问题标题】:Is a memory leak created if a MemoryStream in .NET is not closed?如果 .NET 中的 MemoryStream 未关闭,是否会产生内存泄漏?
【发布时间】:2010-09-19 01:01:49
【问题描述】:

我有以下代码:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

我分配的 MemoryStream 是否有可能在以后无法以某种方式处理掉?

我有一个同行评审坚持要我手动关闭这个,我找不到信息来判断他是否有一个有效的观点。

【问题讨论】:

  • 确切地询问您的审阅者为什么他认为您应该关闭它。如果他谈论一般的良好做法,他可能很聪明。如果他说早点释放内存,那他就错了。
  • 相关问题的意见略有不同,然后在此处的答案中提出:MemoryStream.Close() or MemoryStream.Dispose()。

标签: c# .net memory-leaks memorystream


【解决方案1】:

所有流都实现 IDisposable。将你的内存流包装在 using 语句中,你会很好而且花花公子。 using 块将确保您的流无论如何都关闭和处置。

无论您在哪里调用 Foo,都可以使用 (MemoryStream ms = foo()),我认为您应该还可以。

【讨论】:

  • 我遇到这个习惯的一个问题是你必须确保流没有在其他任何地方使用。例如,我创建了一个指向 MemoryStream 的 JpegBitmapDecoder 并返回 Frames[0] (认为它会将数据复制到自己的内部存储中)但发现位图只会显示 20% 的时间——原来是因为我正在处理内存流。
  • 如果您的内存流必须持续存在(即 using 块没有意义),那么您应该调用 Dispose 并立即将变量设置为 null。如果你丢弃它,那么它就不再被使用了,所以你也应该立即将它设置为 null。 chaiguy 所描述的内容听起来像是一个资源管理问题,因为您不应该分发对某物的引用,除非您将其交给的东西负责处理它,并且分发引用的东西知道它不再负责这样做。
【解决方案2】:

如果一个对象实现了 IDisposable,你必须在完成后调用 .Dispose 方法。

在某些对象中,Dispose 的含义与 Close 的含义相同,反之亦然,在这种情况下,两者都可以。

现在,对于您的特定问题,不,您不会泄漏内存。

【讨论】:

  • “必须”是一个非常强烈的词。只要有规则,就值得知道违反规则的后果。对于 MemoryStream,后果很少。
【解决方案3】:

如果某物是一次性的,则应始终将其丢弃。您应该在您的 bar() 方法中使用 using 语句来确保 ms2 被 Disposed。

它最终会被垃圾收集器清理掉,但调用 Dispose 总是好的做法。如果您在代码上运行 FxCop,它会将其标记为警告。

【讨论】:

  • 使用块调用为你处理。
  • @Grauenwolf:您的断言破坏了封装。作为消费者,你不应该关心它是否是空操作:如果它是 IDisposable,那么 Dispose() 它是你的工作。
  • 这不适用于 StreamWriter 类:仅如果您处置 StreamWriter,它将处置连接的流 - 如果它被垃圾收集并且它永远不会处置流它的终结器被调用——这是设计使然。
  • 我知道这个问题是 2008 年提出的,但今天我们有了 .NET 4.0 任务库。使用任务时,Dispose() 在most cases 中为unnecessary。虽然我同意 IDisposable 应该的意思是“你最好在完成后处理掉它”,但它不再是这个意思了。
  • 另一个你不应该处理 IDisposable 对象的例子是 HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong 只是另一个来自 BCL 的例子,其中有 IDisposable 对象并且你不需要(甚至不应该)处理它。这只是为了记住,即使在 BCL 中,通常也有一些一般规则的例外;)
【解决方案4】:

我不是 .net 专家,但这里的问题可能是资源,即文件句柄,而不是内存。我猜垃圾收集器最终会释放流并关闭句柄,但我认为明确关闭它始终是最佳做法,以确保将内容刷新到磁盘。

【讨论】:

  • A MemoryStream 全部在内存中 - 这里没有文件句柄。
【解决方案5】:

不需要调用.Dispose()(或用Using包装)。

您调用.Dispose()的原因是尽快释放资源。

想想 Stack Overflow 服务器,我们有一组有限的内存和数千个请求进入。我们不想等待计划的垃圾收集,我们希望尽快释放内存因此它可用于新的传入请求。

【讨论】:

  • 在 MemoryStream 上调用 Dispose 不会释放任何内存。事实上,在调用 Dispose 之后,您仍然可以获取 MemoryStream 中的数据 - 试试看:)
  • -1 虽然对于 MemoryStream 来说确实如此,但作为一般建议,这完全是错误的。 Dispose 是释放非托管资源,例如文件句柄或数据库连接。内存不属于这一类。您几乎总是应该等待计划的垃圾回收以释放内存。
  • 采用一种编码风格来分配和处理FileStream 对象和另一种编码风格用于MemoryStream 对象有什么好处?
  • 一个 FileStream 涉及 unmanaged 资源,这些资源实际上可以在调用 Dispose 时立即释放。另一方面,MemoryStream 将 managed 字节数组存储在其 _buffer 变量中,该变量在处理时不会被释放。事实上,在 MemoryStream 的 Dispose 方法中,_buffer 甚至没有被清空,这是一个 SAMEFUL BUG IMO,因为清空引用可以使内存在处理时有资格进行 GC。相反,一个挥之不去的(但已处置的) MemoryStream 引用仍然保留在内存中。因此,一旦你处置它,如果它仍在范围内,你也应该将其设为空。
  • @Triynko - “因此,一旦你处置它,如果它仍在范围内,你也应该将其归零” - 我不同意。如果在调用 Dispose 后再次使用它,这将导致 NullReferenceException。如果 Dispose 后不再使用,则无需为空; GC 足够聪明。
【解决方案6】:

您不会泄漏内存,但您的代码审查员指出您应该关闭您的流是正确的。这样做是有礼貌的。

您可能会泄漏内存的唯一情况是您不小心留下了对流的引用并且从未关闭它。您仍然没有真正泄漏内存,但您不必要地延长了您声称使用它的时间。

【讨论】:

  • > 您仍然没有真正泄漏内存,但是您不必要地延长了您声称使用它的时间。你确定吗? Dispose 不会释放内存,在函数后期调用它实际上可能会延长它无法被收集的时间。
  • 是的,乔纳森说得有道理。在函数后期调用 Dispose 实际上可能会导致编译器认为您需要在函数后期访问流实例(以关闭它)。这可能比根本不调用 dispose 更糟糕(因此避免了对流变量的函数后期引用),因为编译器可以在函数的早期计算最佳释放点(又名“最后可能使用点”) .
【解决方案7】:

你不会泄露任何东西——至少在当前的实现中是这样。

调用 Dispose 不会更快地清理 MemoryStream 使用的内存。它将阻止您的流在调用后可用于读/写调用,这可能对您有用,也可能没用。

如果您绝对确定您从不想要从 MemoryStream 移动到另一种流,那么不调用 Dispose 不会对您造成任何伤害。但是,这通常是一种很好的做法,部分原因是如果您确实更改为使用不同的 Stream,您不希望被难以找到的错误所困扰,因为您早早选择了简单的方法在。 (另一方面,还有 YAGNI 论点……)

这样做的另一个原因是新的实现可能引入了将在 Dispose 上释放的资源。

【讨论】:

  • 在这种情况下,该函数返回一个 MemoryStream,因为它提供了“可以根据调用参数进行不同解释的数据”,因此它可能是一个字节数组,但由于其他原因更容易做一个 MemoryStream。所以它肯定不会是另一个 Stream 类。
  • 在这种情况下,我仍然会尝试按照一般原则处理它 - 养成良好的习惯等 - 但如果它变得棘手,我不会太担心.
  • 如果真的担心尽快释放资源,请在“使用”块之后立即取消引用,以便清理非托管资源(如果有的话),并且对象变得符合条件用于垃圾收集。如果该方法立即返回,它可能不会有太大的不同,但如果您继续在该方法中执行其他操作,例如请求更多内存,那么它肯定会有所作为。
  • @Triynko 不是真的:有关详细信息,请参阅:stackoverflow.com/questions/574019/…。
  • YAGNI 论点可以采用两种方式——因为决定不处理实现 IDisposable 的东西是违反正常最佳实践的特殊情况,您可以争辩说您不应该这样做根据 YAGNI 原则,在你真正需要之前不要这样做。
【解决方案8】:

我建议将 MemoryStream 包装在 bar() 中的 using 语句中,主要是为了保持一致性:

  • 现在 MemoryStream 不会释放 .Dispose() 上的内存,但有可能在将来的某个时候它可能会,或者您(或您公司的其他人)可能会用您自己的自定义 MemoryStream 替换它,等
  • 它有助于在您的项目中建立一种模式,以确保所有流都得到处理——通过说“必须处理所有流”而不是“必须处理一些流”来更明确地划定界限,但某些人不必“...
  • 如果您曾经更改代码以允许返回其他类型的流,则无论如何您都需要将其更改为 dispose。

在创建和返回 IDisposable 时,我通常在 foo() 这样的情况下做的另一件事是确保构造对象和 return 之间的任何失败都被异常捕获,释放对象并重新抛出异常:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}

【讨论】:

    【解决方案9】:

    在垃圾收集语言中,非托管资源的处置是不确定的。即使您显式调用 Dispose,您也绝对无法控制实际释放后备内存的时间。 Dispose 在对象超出范围时被隐式调用,无论是通过退出 using 语句,还是从从属方法弹出调用堆栈。综上所述,有时对象实际上可能是托管资源(例如文件)的包装器。这就是为什么最好在 finally 语句中显式关闭或使用 using 语句的原因。 干杯

    【讨论】:

    • 不完全正确。退出 using 语句时调用 Dispose。当对象刚刚超出范围时,不会调用 Dispose。
    【解决方案10】:

    这已经回答了,但我只是补充一点,信息隐藏的老式良好原则意味着您可能在未来的某个时间点想要重构:

    MemoryStream foo()
    {    
        MemoryStream ms = new MemoryStream();    
        // write stuff to ms    
        return ms;
    }
    

    到:

    Stream foo()
    {    
       ...
    }
    

    这强调调用者不应该关心返回的是什么类型的 Stream,并且可以更改内部实现(例如,在模拟单元测试时)。

    如果您没有在 bar 实现中使用 Dispose,那么您将遇到麻烦:

    void bar()
    {    
        using (Stream s = foo())
        {
            // do stuff with s
            return;
        }
    }
    

    【讨论】:

      【解决方案11】:

      是的,有一个泄漏,这取决于您如何定义 LEAK 以及您的意思是多长时间...

      如果泄漏是指“内存仍处于分配状态,无法使用,即使您已经使用完毕”,而后者是指在调用 dispose 后的任何时间,那么是的,可能存在泄漏,尽管它不是永久性的(即在您的应用程序运行时的整个生命周期内)。

      要释放 MemoryStream 使用的托管内存,您需要取消对它的引用,方法是取消对它的引用,这样它就可以立即进行垃圾回收了。如果你没有做到这一点,那么你会在你完成使用它时创建一个临时泄漏,直到你的引用超出范围,因为在此期间内存将无法用于分配。

      using 语句(而不是简单地调用 dispose)的好处是您可以在 using 语句中声明您的引用。当 using 语句完成时,不仅调用 dispose,而且您的引用超出范围,有效地使引用无效并使您的对象立即符合垃圾回收条件,而无需您记住编写“reference=null”代码。

      虽然未能立即取消引用某些内容并不是经典的“永久性”内存泄漏,但它肯定具有相同的效果。例如,如果您保留对 MemoryStream 的引用(即使在调用 dispose 之后),并且在您的方法中再往下一点,您尝试分配更多内存......您仍然引用的内存流正在使用的内存将不可用直到您取消引用或它超出范围,即使您调用了 dispose 并完成使用它。

      【讨论】:

      • 我喜欢这个回复。有时人们会忘记使用的双重职责:急切的资源回收和急切的解引用。
      • 确实,虽然我听说 C# 编译器与 Java 不同,会检测“最后一次可能使用”,所以如果变量在最后一次引用后注定要超出范围,它可能会成为垃圾回收的条件在它最后一次可能的使用之后......在它实际超出范围之前。见stackoverflow.com/questions/680550/explicit-nulling
      • 垃圾收集器和抖动不是这样工作的。范围是一种语言结构,而不是运行时会遵守的东西。事实上,您可能通过在块结束时添加对 .Dispose() 的调用来延长引用在内存中的时间。见ericlippert.com/2015/05/18/…
      【解决方案12】:

      MemorySteram 只不过是字节数组,它是托管对象。 忘记 dispose 或 close 这除了结束完成之外没有任何副作用。
      只需检查反射器中 MemoryStream 的构造函数或刷新方法,就会清楚为什么您不需要担心关闭或处置它,而不仅仅是为了遵循良好实践。

      【讨论】:

      • -1:如果您要发布一个 4 岁以上的问题且答案已被接受,请尝试使其变得有用。
      猜你喜欢
      • 2020-04-14
      • 1970-01-01
      • 1970-01-01
      • 2010-09-06
      • 1970-01-01
      • 2012-03-27
      • 1970-01-01
      • 2022-11-04
      • 2021-07-15
      相关资源
      最近更新 更多