9

这是一个非常基本的问题。我将使用 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 值。我可以在调试器中暂停我的程序并检查 obj1、obj2 的 refcount 并了解存在问题。我还可以在对象的析构函数中设置断点并观察破坏时刻(或发现该对象尚未被破坏)。

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

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

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

最后,问题本身:

爪哇:

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

6 回答 6

8

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

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

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

  • 正如您所观察到的,它无法处理循环引用。
  • 它有大量的内存和线程开销来准确维护。
  • 对于托管内存,有更好、更简单的方法来处理这种情况。

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

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

于 2013-05-01T10:38:19.583 回答
5

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

于 2013-05-01T09:53:10.973 回答
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 使用它来查找所有可能被引用的对象。然后垃圾收集器递归地向下迭代对象树,将“示例”标记为对垃圾收集器根的依赖。

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

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

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

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

于 2015-12-24T02:04:48.197 回答
1

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

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

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

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

重新 - 你的最后一个问题 - 这不是内存泄漏。当 GC 启动时,除了可访问的内容外,所有内容都将被丢弃。在这种情况下,假设您已经释放obj1并且obj2两者都无法访问,因此它们都将被丢弃。

于 2013-05-01T10:02:04.100 回答
1

垃圾收集不是简单的 ref 计数

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

于 2013-05-01T10:43:54.310 回答
0

垃圾收集语言不允许您检查 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#/etc。程序员必须这样做(因为他们必须手动关闭/释放内存以外的资源)。我知道那里'

于 2013-05-01T11:07:11.713 回答