垃圾收集与手动内存管理

时间:2013-05-01 09:47:44

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

这是一个非常基本的问题。我将使用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;
}

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

但是有一个积极的事情:程序员可以完全控制refcount值。我可以在调试器中暂停我的程序并检查obj1,obj2的引用计数并理解存在问题。我也可以在对象的析构函数中设置断点并观察破坏时刻(或者发现该对象没有被破坏)。

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

  1. 不要让我检查对象的引用计数
  2. 当对象被销毁时(不管怎样,当对象暴露给GC时)不要让我知道。
  3. 我经常听说这些语言不允许程序员泄漏内存,这就是为什么它们很棒。据我了解,他们只是隐藏了内存管理问题,并且很难解决它们。

    最后,问题本身:

    爪哇:

    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. 如果不是:为什么?

6 个答案:

答案 0 :(得分:8)

托管内存系统建立在您不希望首先跟踪内存泄漏问题的假设之上。而不是让它们更容易解决,你试着确保它们永远不会发生在一起。

Java对于“内存泄漏”确实有一个失败的术语,这意味着内存中的任何增长可能会影响您的应用程序,但是管理内存无法清除所有内存。

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

  • 它无法像你观察到的那样处理循环引用。
  • 它具有显着的内存和线程开销,可以准确维护。
  • 有更好,更简单的方法来处理托管内存的这种情况。

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

相反,Java会跟踪许多根上下文(例如每个线程堆栈),并且可以跟踪哪些对象需要保留,哪些对象可以根据这些对象是否可以高度访问而被丢弃。它还为弱引用提供了工具(只要对象没有被清理就会被保留)和软引用(通常不会被清理但可以由垃圾收集者自行决定)

答案 1 :(得分:5)

AFAIK,Java GC的工作原理是从一组明确定义的初始引用开始,并计算可以从这些引用中获得的对象的传递闭包。任何无法访问的内容都是“泄露”的,可以进行GC编辑。

答案 2 :(得分:2)

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 ++中被释放)。

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

这些是GC'根',java用它来查找可能被引用的所有对象。垃圾收集器然后以递归方式向下迭代对象树,将“example”标记为对垃圾收集器根添加成活动。

垃圾收集器会破坏所有其他对象。 这确实会带来性能损失,但此过程已经过大量优化,对大多数应用程序而言并不重要。

与C ++不同,您不必担心所有的引用周期,因为只有从GC根可以访问的对象才会存在。

使用java应用程序,你必须担心内存(思考列表保留所有迭代中的对象),但它没有其他语言那么重要。

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

答案 3 :(得分:1)

关键区别在于Java等你根本没有涉及处理问题。这可能感觉像一个非常可怕的位置,但令人惊讶的是赋予权力。您过去必须做出的关于谁负责处理已创建对象的所有决定都已消失。

它确实有意义。系统更了解什么是可达的,什么不是你。它还可以在何时拆除结构等方面做出更加灵活和明智的决策。

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

作为一名前C程序员,我感到很痛苦。

重新 - 你的最后一个问题 - 这不是内存泄漏。当GC开始时,除了可到达的内容之外,所有内容都会被丢弃。在这种情况下,假设您已发布obj1obj2两者都不可访问,因此它们都将被丢弃。

答案 4 :(得分:1)

垃圾收集不是简单的重新计算

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

答案 5 :(得分:0)

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

{
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();
  }
}

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