【问题标题】:Garbage collection vs manual memory management垃圾收集与手动内存管理
【发布时间】:2013-04-25 07:43:19
【问题描述】:

这是一个非常基本的问题。我将使用 C++ 和 Java 来制定它,但它确实与语言无关。 考虑 C++ 中的一个众所周知的问题:

struct Obj
{
    boost::shared_ptr<Obj> m_field;
};

{
    boost::shared_ptr<Obj> obj1(new Obj);
    boost::shared_ptr<Obj> obj2(new Obj);
    obj1->m_field = obj2;
    obj2->m_field = obj1;
}

这是内存泄漏,每个人都知道 :)。解决方案也是众所周知的:应该使用弱指针来打破“引用计数互锁”。众所周知,这个问题原则上不能自动解决。解决它完全是程序员的责任。

但是有一件好事:程序员可以完全控制引用计数值。我可以在调试器中暂停我的程序并检查 obj1、obj2 的引用计数并了解存在问题。我还可以在对象的析构函数中设置断点并观察销毁时刻(或发现对象尚未被销毁)。

我的问题是关于 Java、C#、ActionScript 和其他“垃圾收集”语言。我可能会遗漏一些东西,但在我看来他们

  1. 不要让我检查对象的引用计数
  2. 当对象被销毁时不要让我知道(好的,当对象暴露给 GC 时)

我经常听到这些语言不允许程序员泄漏内存,这就是它们很棒的原因。据我了解,它们只是隐藏了内存管理问题并使其难以解决。

最后,问题本身:

Java:

public class Obj
{
    public Obj m_field;
}

{
     Obj obj1 = new Obj();
     Obj obj2 = new Obj();
     obj1.m_field = obj2;
     obj2.m_field = obj1;
}
  1. 是内存泄漏吗?
  2. 如果是:如何检测和修复它?
  3. 如果不是:为什么?

【问题讨论】:

  • 这不是内存泄漏。它不会保护您避免内存泄漏,但没有什么可以阻止您在析构函数中释放这些对象。内存管理是应用程序设计的一部分;低级技巧无法弥补设计上的不足。
  • 不允许程序员泄漏内存不是这样的,但是这些语言可以在大多数情况下保护您免受内存泄漏。这对于那些没有的程序员来说是一个很大的优势任何关于内存的想法,我们至少在分配一些小项目时不需要太担心内存泄漏
  • 无法访问引用计数,因为大多数实现不维护引用计数,并且语言通常注意不要限制比绝对必要更多的实现细节(因为这会阻止更好的实现——更快、更健壮,更有用等)。

标签: java c++ memory-management memory-leaks jvm


【解决方案1】:

AFAIK,Java GC 的工作原理是从一组定义良好的初始引用开始,并计算可以从这些引用到达的对象的传递闭包。任何无法到达的东西都会被“泄露”并且可以被 GC-ed。

【讨论】:

  • 也许它们是为 Java 定义的,但对我来说不是:)。我在哪里可以了解有关这些参考资料的更多信息?
  • @Nick 我不使用 Java,所以很遗憾我不知道。试试 Java 文档或 Google 叔叔。
  • 一般垃圾收集的基本参考是The Garbage Collection Handbook,作者是 Richard Jones、Antony Hosking 和 Eliot Moss。它对大多数(如果不是全部)各种内存管理策略(包括 shared_ptr 使用的引用计数)进行了广泛的讨论。
【解决方案2】:

关键的区别在于,在 Java 等中您根本不参与处理问题。这可能感觉像是一个非常可怕的位置,但它令人惊讶地授权。您过去必须做出的关于谁负责处置创建的对象的所有决定都已不复存在。

这确实是有道理的。系统比您更了解什么是可到达的,什么是不可到达的。它还可以就何时拆除结构等做出更加灵活和智能的决策。

本质上 - 在这种环境中,您可以以更复杂的方式处理对象,而不必担心丢失对象。您现在唯一需要担心的是,如果您不小心将一个粘到天花板上。

作为一名迁移到 Java 的前 C 程序员,我感受到了你的痛苦。

Re - 你的最后一个问题 - 这不是内存泄漏。当 GC 启动时,所有东西都会被丢弃,除了可以到达的东西。在这种情况下,假设您已释放 obj1obj2 两者都无法访问,因此它们都将被丢弃。

【讨论】:

  • “系统比你更了解什么是可达的,什么是不可达的”。嗯...听起来不错,直到我没有记忆障碍。但是当我的程序内存不足时我该怎么办?我确实知道如何在“类似 C++”的环境中调查和优化内存管理。但是我应该在 Java 中做什么?
  • @Nick 在 Java 中,您应该 a) 增加内存量或 b) 优化程序以使用更少的内存。无论哪种情况,您都可以使用内存分析器来确定最佳解决方案。 Java 带有一个免费的 VisualVM,您可以将其附加到任何正在运行的进程,虽然它不是很好,但它是一个很好的起点。优化内存使用是 Java 要么 a) 更简单,因为 C++ 中的许多问题都不会发生,要么 b) 更难,因为你没有那么多方法来压缩系统的每个最后一个字节。假设您以 300 美元购买 32 GB,我选择 a)
【解决方案3】:

托管内存系统建立在您不希望首先跟踪内存泄漏问题的假设之上。与其让它们更容易解决,不如从一开始就确保它们永远不会发生。

Java 确实有一个“内存泄漏”的术语,这意味着任何可能影响您的应用程序的内存增长,但从来没有一点托管内存无法清理所有内存。

出于多种原因,JVM 不使用引用计数

  • 如您所见,它无法处理循环引用。
  • 它需要大量内存和线程开销才能准确维护。
  • 有更好、更简单的方法来处理托管内存的此类情况。

虽然 JLS 不禁止使用引用计数,但它并未在任何 JVM AFAIK 中使用。

相反,Java 会跟踪一些根上下文(例如每个线程堆栈),并且可以根据这些对象是否强可达来跟踪哪些对象需要保留,哪些可以丢弃。它还为弱引用(只要对象未清理就保留)和软引用(通常不清理但可由垃圾收集器自行决定)提供便利

【讨论】:

  • Rust 可能值得一提。
【解决方案4】:

垃圾回收不是简单的引用计数

您演示的循环引用示例不会出现在垃圾收集托管语言中,因为垃圾收集器将希望一直跟踪分配引用回到堆栈上的某些内容。 如果某处没有堆栈引用,那就是垃圾。像shared_ptr 这样的引用计数系统并不那么聪明,并且有可能(就像你演示的那样)在堆中的某处有两个对象,它们可以防止彼此被删除。

【讨论】:

    【解决方案5】:

    垃圾收集语言不允许您检查 refcounter,因为它们没有任何内容。垃圾收集与引用内存管理完全不同。真正的区别在于确定性。

    {
    std::fstream file( "example.txt" );
    // do something with file
    }
    // ... later on
    {
    std::fstream file( "example.txt" );
    // do something else with file
    }
    

    在 C++ 中,您可以保证 example.txt 在第一个块关闭后已关闭,或者如果抛出异常。与Java比较

    {
    try 
      {
      FileInputStream file = new FileInputStream( "example.txt" );
      // do something with file
      }
    finally
      {
      if( file != null )
        file.close();
      }
    }
    // ..later on
    {
    try 
      {
      FileInputStream file = new FileInputStream( "example.txt" );
      // do something with file
      }
    finally
      {
      if( file != null )
        file.close();
      }
    }
    

    如您所见,您已将内存管理换成了所有其他资源管理。那是真正的区别,被引用的对象仍然保持确定性破坏。在垃圾收集语言中,您必须手动释放资源并检查异常。有人可能会争辩说,显式内存管理可能很乏味且容易出错,但在现代 C++ 中,智能指针和标准容器可以缓解这种情况。你仍然有一些责任(例如循环引用),但想想有多少个 catch/finally 块可以避免使用确定性破坏以及多少键入 Java/C#/等。程序员必须这样做(因为他们必须手动关闭/释放内存以外的资源)。而且我知道 C# 中有 using 语法(在最新的 Java 中也有类似的语法),但它仅涵盖块作用域的生命周期,而不是更普遍的共享所有权问题。

    【讨论】:

      【解决方案6】:

      Java 具有独特的内存管理策略。所有的东西(除了一些特定的东西)都在堆上分配,直到 GC 开始工作才被释放。

      例如:

      public class Obj {
          public Object example;
          public Obj m_field;
      }
      
      public static void main(String[] args) {
          int lastPrime = 2;
          while (true) {
              Obj obj1 = new Obj();
              Obj obj2 = new Obj();
              obj1.example = new Object();
              obj1.m_field = obj2;
              obj2.m_field = obj1;
              int prime = lastPrime++;
              while (!isPrime(prime)) {
                  prime++;
              }
              lastPrime = prime;
              System.out.println("Found a prime: " + prime);
          }
      }
      

      C 通过要求您手动释放两个“obj”的内存来处理这种情况,C++ 计算对“obj”的引用并在它们超出范围时自动销毁它们。 Java 确实释放这块内存,至少一开始没有。

      Java 运行时会等待一段时间,直到感觉使用了太多内存。之后,垃圾收集器开始工作。

      假设 Java 垃圾收集器决定在外循环的第 10,000 次迭代后进行清理。此时,已经创建了 10,000 个对象(在 C/C++ 中这些对象已经被释放)。

      虽然外循环有 10000 次迭代,但只有新创建的 obj1 和 obj2 可能被代码引用。

      这些是 GC 的“根”,java 使用它来查找所有可能被引用的对象。垃圾收集器然后递归地向下迭代对象树,将“示例”标记为对垃圾收集器根的依赖。

      所有其他对象随后被垃圾收集器销毁。 这确实会带来性能损失,但这个过程已经过高度优化,对大多数应用程序来说并不重要。

      与 C++ 不同,您完全不必担心引用循环,因为只有可从 GC 根访问的对象才会存在。

      对于 java 应用程序,您确实不得不担心内存(想想保存所有迭代的对象的列表),但它不像其他语言那么重要。

      至于调试:Java 调试高内存值的想法是使用特殊的“内存分析器”来找出哪些对象仍在堆上,担心什么在引用什么。

      【讨论】:

      • Java 的内存管理策略并不是独一无二的。垃圾收集可以追溯到 Lisp。此外,不同的 JVM 可以实现不同的垃圾回收。
      • @JayElston Lisp 发明了垃圾收集?而且我知道这是一种简化,JVM 在如何收集杂物方面有很大的余地:)
      • 如果你相信Wikipedia :-)
      猜你喜欢
      • 1970-01-01
      • 2012-11-30
      • 1970-01-01
      • 2011-10-22
      • 1970-01-01
      • 2010-09-19
      • 2012-04-30
      • 2011-02-28
      • 2011-02-26
      相关资源
      最近更新 更多