为什么垃圾回收仅扩展到内存而不扩展到其他资源类型?


12

似乎人们厌倦了手动内存管理,于是他们发明了垃圾回收器,生活还算不错。但是其他所有资源类型呢?文件描述符,套接字,甚至用户创建的数据(如数据库连接)?

这听起来像是一个幼稚的问题,但是我找不到任何有人问过的地方。让我们考虑文件描述符。假设某个程序知道启动时仅允许有4000 fds。每当它执行将打开文件描述符的操作时,如果会

  1. 检查以确保它不会用完。
  2. 如果是这样,则触发垃圾回收器,这将释放一堆内存。
  3. 如果某些内存释放了对文件描述符的保留引用,请立即将其关闭。它知道该内存属于某个资源,因为与该资源绑定的内存在首次打开时就注册了一个“文件描述符注册表”,但缺乏更好的用语。
  4. 打开一个新的文件描述符,将其复制到新的内存中,将该内存位置注册到“文件描述符注册表”中,并将其返回给用户。

因此,资源不会立即被释放,但是只要gc至少在资源即将用完之前就运行,只要它没有被完全利用,它将至少被释放。

对于许多用户定义的资源清理问题而言,这似乎就足够了。我设法在这里找到一条注释,该注释在C ++中使用类似于包含对资源的引用的线程的方式进行类似于C ++的清理,并在仅剩一个引用(来自清理线程)时对其进行清理,但是我可以找不到任何证据表明这是图书馆或任何现有语言的一部分。

Answers:


4

GC处理可预测和保留的资源。VM对其具有完全控制权,并完全控制创建哪些实例以及何时创建实例。此处的关键字为“保留”和“完全控制”。句柄是由OS分配的,而指针则是指向在托管空间之外分配的资源的指针。因此,句柄和指针不限于在托管代码中使用。它们可以(并且经常)由在同一进程上运行的托管和非托管代码使用。

“资源收集器”将能够验证是否在托管空间内使用了句柄/指针,但是根据定义,它不知道其内存空间之外发生了什么(而且,更糟糕的是,可以使用某些句柄)跨过程边界)。

一个实际的例子是.NET CLR。可以使用调味的C ++编写可同时用于托管和非托管内存空间的代码。句柄,指针和引用可以在托管和非托管代码之间传递。非托管代码必须使用特殊的构造/类型,以允许CLR跟踪对其托管资源的引用。但这是最好的。它不能对句柄和指针做同样的事情,因此,资源收集器将不知道是否可以释放特定的句柄或指针。

编辑:关于.NET CLR,我对使用.NET平台进行C ++开发没有经验。也许有一些特殊的机制可以使CLR跟踪对托管和非托管代码之间的句柄/指针的引用。如果是这种情况,那么CLR可以照顾这些资源的生命周期,并在所有对它们的引用都被清除时释放它们(嗯,至少在某些情况下可以)。无论哪种方式,最佳实践都要求在不需要它们时应立即释放句柄(尤其是指向文件的句柄)和指针。资源收集器将不遵守该要求,这是另一个原因。

编辑2:在CLR / JVM / VMs-general上,如果仅在托管空间内使用某些代码来释放特定的句柄,则相对来说比较琐碎。在.NET中将是这样的:

// This class offends many best practices, but it would do the job.
public class AutoReleaseFileHandle {
    // keeps track of how many instances of this class is in memory
    private static int _toBeReleased = 0;

    // the threshold when a garbage collection should be forced
    private const int MAX_FILES = 100;

    public AutoReleaseFileHandle(FileStream fileStream) {
       // Force garbage collection if max files are reached.
       if (_toBeReleased >= MAX_FILES) {
          GC.Collect();
       }
       // increment counter
       Interlocked.Increment(ref _toBeReleased);
       FileStream = fileStream;
    }

    public FileStream { get; private set; }

    private void ReleaseFileStream(FileStream fs) {
       // decrement counter
       Interlocked.Decrement(ref _toBeReleased);
       FileStream.Close();
       FileStream.Dispose();
       FileStream = null;
    }

    // Close and Dispose the Stream when this class is collected by the GC.
    ~AutoReleaseFileHandle() {
       ReleaseFileStream(FileStream);
    }

    // because it's .NET this class should also implement IDisposable
    // to allow the user to dispose the resources imperatively if s/he wants 
    // to.
    private bool _disposed = false;
    public void Dispose() {
      if (_disposed) {
        return;
      }
      _disposed = true;
      // tells GC to not call the finalizer for this instance.
      GC.SupressFinalizer(this);

      ReleaseFileStream(FileStream);
    }
}

// use it
// for it to work, fs.Dispose() should not be called directly,
var fs = File.Open("path/to/file"); 
var autoRelease = new AutoReleaseFileHandle(fs);

3

这似乎是带有垃圾回收器的语言实现终结器的原因之一。终结器旨在允许程序员在垃圾回收期间清理对象的资源。终结器的最大问题是不能保证它们可以运行。

这里有一个关于使用终结器的不错的文章:

对象完成和清理

实际上,它专门以文件描述符为例。您应该确保自己清理此类资源,但是有一种机制可以还原未正确释放的资源。


我不确定这是否能回答我的问题。它缺少我的建议部分,即系统知道该资源即将用完。完善这一部分的唯一方法是确保在分配新文件描述符之前手动运行gc,但这效率极低,而且我不知道您是否可以使gc在Java中运行。
mindreader

可以,但是文件描述符通常在操作系统中表示一个打开文件,这意味着(取决于OS)使用系统级资源,例如锁,缓冲池,结构池等。坦白说,我看不出将这些结构开放供以后进行垃圾回收的好处,而且我看到许多不利因素,使它们的分配时间超出了必要。如果程序员忽略了清理资源的调用,但不应依赖Finalize()方法,以允许进行最后的沟渠清理。
布莱恩·希伯特

我的理解是不应该依赖它们的原因是,如果您要分配大量的这些资源,例如您可能正在向下打开每个文件的文件层次结构,则可能在gc发生之前打开了太多文件运行,引起爆炸。内存也会发生同样的事情,除了运行时检查以确保不会耗尽内存。我想知道为什么系统无法实现在爆裂之前以与完成内存几乎相同的方式回收任意资源。
mindreader

可以将系统写入内存以外的GC资源,但是您必须跟踪引用计数或使用其他方法确定何时不再使用资源。您不想取消分配和重新分配仍在使用的资源。如果线程打开了要写入的文件,操作系统“回收”了文件句柄,而另一个线程使用相同的句柄打开了另一个文件进行写入,则可能会出现混乱。而且我还建议不要将它们保持打开状态,直到有类似GC的线程来释放它们为止,这是浪费大量资源。
布莱恩·希伯特

3

有许多编程技术可以帮助管理这类资源。

  • C ++程序员经常使用一种称为Resource Acquisition is Initialization的模式,简称RAII。这种模式可确保当持有资源的对象超出范围时,它将关闭其持有的资源。当对象的生存期对应于程序中的特定范围时(例如,当它与特定的堆栈框架出现在堆栈上的时间匹配时),这将很有帮助,因此对于局部变量指向的对象(指针)有帮助变量存储在堆栈上),但对于由堆上存储的指针指向的对象不是很有用。

  • Java,C#和许多其他语言提供了一种方法,用于指定当某个对象不再存在并且即将由垃圾收集器收集时将被调用的方法。参见例如终结器dispose(),和其他。想法是程序员可以实现这种方法,以便在垃圾回收器释放对象之前,它将显式关闭资源。但是,这些方法确实存在一些问题,您可以在其他地方阅读这些问题。例如,垃圾收集器可能要到比您想要的时间晚得多的时候才收集对象。

  • C#和其他语言提供了一个using关键字,可帮助确保在不再需要资源后将其关闭(因此,您不要忘记关闭文件描述符或其他资源)。这通常比依靠垃圾收集器发现对象不再存在要好。参见例如/programming//q/75401/781723。这里的总称是托管资源。这个概念建立在RAII和终结器的基础上,在某些方面对其进行了改进。


我对即时资源的重新分配不感兴趣,而对及时分配的想法更感兴趣。RIAA非常棒,但不适用于许多垃圾回收语言。Java缺少知道何时将耗尽某种资源的能力。使用和括号类型操作非常有用,可以处理错误,但是我对它们不感兴趣。我只想分配资源,然后在方便或有必要时它们就会清理自己,并且几乎没有办法解决它。我想没有人真的对此进行过调查。
mindreader

2

所有内存都是相等的,如果我要求1K,则我不在乎1K在地址空间中的哪个位置。

当我请求文件句柄时,我想要一个要打开的文件的句柄。在文件上打开文件句柄通常会阻止其他进程或机器对该文件的访问。

因此,必须在不需要文件句柄时立即将其关闭,否则它们将阻止对文件的其他访问,但是仅在开始用尽该文件句柄时才需要回收内存。

运行GC传递的成本很高,并且只能在“需要时”完成,因此无法预测其他进程何时需要一个文件句柄,表明您的进程可能不再使用,但仍处于打开状态。


您的答案很关键:内存是可互换的,并且大多数系统都具有足够的内存,因此不需要特别快地回收它。相比之下,如果某个程序获得了对文件的独占访问权,那么它将阻止Universe中可能需要使用该文件的所有其他程序,无论可能存在多少其他文件。
超级猫

0

我猜想为什么其他资源没有得到太多使用的原因恰恰是因为大多数其他资源都希望尽快发布以供任何人重用。

注意,当然,现在可以使用现有GC技术使用“弱”文件描述符来提供您的示例。


0

检查是否不再可以访问内存(从而保证不再使用内存)非常容易。大多数其他类型的资源都可以使用或多或少的相同技术来处理(即,资源获取是初始化,RAII,以及销毁用户时释放的对应对象,这与内存管理联系起来)。通常,不可能进行某种“及时”释放(检查暂停问题,您将不得不发现上次使用了某些资源)。是的,有时它可以自动完成,但是作为内存,情况要复杂得多。因此,它在很大程度上依赖于用户干预。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.