ガベージコレクションがメモリのみに拡張され、他のリソースタイプには拡張されないのはなぜですか?


12

手動のメモリ管理にうんざりしているようで、ガベージコレクションを発明し、生活はかなり良かったです。しかし、他のすべてのリソースタイプはどうですか?ファイル記述子、ソケット、またはデータベース接続などのユーザー作成データですか?

これは素朴な質問のように思えますが、誰かが質問した場所はどこにもありません。ファイル記述子について考えてみましょう。プログラムが起動時にのみ4000 fdsを使用できるようになることをプログラムが知っているとします。ファイル記述子を開く操作を実行するときはいつでも、

  1. 間もなく実行されることを確認してください。
  2. そうであれば、ガベージコレクターをトリガーして、大量のメモリを解放します。
  3. 解放されたメモリの一部がファイル記述子への参照を保持していた場合は、それらをすぐに閉じます。リソースに関連付けられているメモリは、最初に開かれたときに、より適切な用語がないため、「ファイル記述子レジストリ」に登録されていたため、メモリはリソースに属していることがわかります。
  4. 新しいファイル記述子を開き、それを新しいメモリにコピーし、そのメモリ位置を「ファイル記述子レジストリ」に登録して、ユーザーに返します。

したがって、リソースはすぐには解放されませんが、リソースが完全に使用されていない場合、少なくともリソースがなくなる直前に、gcが実行されるたびに解放されます。

そして、それは多くのユーザー定義のリソースクリーンアップ問題には十分だと思われます。私はここで、リソースへの参照を含むスレッドでC ++でこれと同様のクリーンアップを実行することを参照する単一のコメントを見つけ、単一の参照のみが残っている場合に(クリーンアップスレッドから)クリーンアップしますが、これがライブラリまたは既存の言語の一部であることの証拠を見つけます。

回答:


4

GCは、予測可能で予約されたリソースを扱います。VMはVMを完全に制御し、作成するインスタンスと作成するインスタンスを完全に制御します。ここのキーワードは「予約」と「トータルコントロール」です。ハンドルはOSによって割り当てられ、ポインターは...管理領域外に割り当てられたリソースへのポインターです。そのため、ハンドルとポインターは、マネージコード内での使用に制限されません。それらは、同じプロセスで実行されるマネージコードとアンマネージコードによって使用でき、多くの場合は使用できます。

「リソースコレクター」は、ハンドル/ポインターがマネージドスペース内で使用されているかどうかを確認できますが、定義上、メモリスペースの外で何が起こっているかを認識していません(さらに、いくつかのハンドルを使用すると、事態が悪化します)プロセスの境界を越えて)。

実用的な例は.NET CLRです。フレーバー付きC ++を使用して、マネージメモリスペースとアンマネージメモリスペースの両方で機能するコードを記述できます。ハンドル、ポインタ、参照は、マネージコードとアンマネージコードの間で受け渡しできます。アンマネージコードは、CLRがそのマネージリソースに対して行われた参照を追跡し続けることができるように、特別な構成要素/型を使用する必要があります。しかし、それができる最善の方法です。ハンドルとポインターを使用して同じことを行うことはできません。そのため、リソースコレクターは、特定のハンドルまたはポインターを解放してもよいかどうかはわかりません。

編集:.NET CLRに関しては、.NETプラットフォームでのC ++開発の経験はありません。おそらく、CLRがマネージコードとアンマネージコードの間のハンドル/ポインターへの参照を追跡し続けることを可能にする特別なメカニズムがあるはずです。その場合は、CLRがそれらのリソースの有効期間を処理し、それらへのすべての参照がクリアされたときにそれらを解放できます(少なくとも一部のシナリオでは可能です)。どちらの方法でも、ベストプラクティスでは、ハンドル(特にファイルを指すもの)とポインターは、必要がなくなったらすぐに解放するように指示されています。リソースコレクターはこれに準拠していません。これが、リソースコレクターがない理由の1つです。

編集2:管理された領域内でのみ使用される場合に特定のハンドルを解放するコードを記述することは、CLR / JVM / VMs-in-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

これは、ガベージコレクターを備えた言語がファイナライザーを実装する理由の1つであると思われます。ファイナライザは、プログラマがガベージコレクション中にオブジェクトのリソースをクリーンアップできるようにすることを目的としています。ファイナライザの大きな問題は、ファイナライザの実行が保証されていないことです。

ここにファイナライザの使用に関するかなり良い記事があります:

オブジェクトのファイナライズとクリーンアップ

実際、具体的にはファイル記述子を例として使用しています。このようなリソースは自分でクリーンアップする必要がありますが、適切に解放されなかったリソースを復元できるメカニズムがあります。


これが私の質問に答えるかどうかはわかりません。リソースが不足しそうであることをシステムが認識している私の提案の一部が欠けています。その部分に手を加える唯一の方法は、新しいファイル記述子を割り当てる前に手動でgcを実行することを確認することですが、これは非常に非効率的であり、gcをjavaで実行できるかどうかさえわかりません。
2016

大丈夫ですが、ファイル記述子は通常、オペレーティングシステムのオープンファイルを表します。これは、OSに応じて、ロック、バッファプール、構造プールなどのシステムレベルのリソースを使用することを意味します。率直に言って、これらの構造を後でガベージコレクションのために開いたままにしておくことの利点はわかりません。また、必要以上に割り当てられたままにしておくことに多くの不利益を感じています。Finalize()メソッドは、プログラマーがリソースをクリーンアップするための呼び出しを見落とした場合に最後の溝をクリーンアップできるようにすることを目的としていますが、これに依存すべきではありません。
ブライアンヒバート

私の理解では、それらに依存すべきではない理由は、これらのリソースを大量に割り当てる場合、たとえば、各ファイルを開いているファイル階層を下っていくと、GCが発生する前に多くのファイルを開く可能性があるためです。実行すると、爆発を引き起こします。ランタイムでメモリ不足が発生しないことを確認することを除いて、メモリでも同じことが起こります。メモリが実行されるのとほぼ同じ方法で、システムがブローアップ前に任意のリソースを再利用するように実装できない理由を知りたいのですが。
マインドリーダー2016

システムはメモリ以外のGCリソースに書き込むことができますが、参照カウントを追跡するか、リソースが使用されなくなった時期を特定する他の方法が必要になります。まだ使用されているリソースの割り当てを解除したり、再割り当てしたりする必要はありません。スレッドが書き込み用に開いているファイルを持ち、OSがファイルハンドルを「再利用」し、別のスレッドが同じハンドルを使用して別のファイルを書き込み用に開く場合、騒乱のすべてのマナーが続く可能性があります。そして、GCのようなスレッドが解放して解放するまで、それらを開いたままにしておくことは、かなりのリソースの浪費であることも、やはり示唆します。
ブライアンヒバート

3

これらの種類のリソースの管理に役立つ多くのプログラミング手法があります。

  • C ++プログラマーは、しばしばResource Acquisition is Initialization(略してRAII)というパターンを使用します。このパターンにより、リソースを保持しているオブジェクトがスコープ外になると、保持していたリソースが確実に閉じられます。これは、オブジェクトの存続期間がプログラムの特定のスコープに対応する場合(たとえば、特定のスタックフレームがスタックに存在する時間と一致する場合)に役立つため、ローカル変数(ポインタースタックに格納されている変数)が、ヒープに格納されているポインタが指すオブジェクトにはあまり役立ちません。

  • Java、C#、および他の多くの言語は、オブジェクトがもはや存在せず、ガベージコレクターによって収集されようとしているときに呼び出されるメソッドを指定する方法を提供します。たとえば、ファイナライザ、dispose()などを参照してください。プログラマがそのようなメソッドを実装して、オブジェクトがガベージコレクターによって解放される前にリソースを明示的に閉じることができるという考え方です。ただし、これらのアプローチにはいくつかの問題があります。たとえば、ガベージコレクターは、希望するよりもはるかに遅くなるまでオブジェクトを収集しない可能性があります。

  • C#およびその他の言語には、usingリソースが不要になった後に確実に閉じるためのキーワードが用意されています(そのため、ファイル記述子またはその他のリソースを閉じることを忘れないでください)。これは多くの場合、オブジェクトがもう存在していないことを発見するためにガベージコレクターに依存するよりも優れています。たとえば、/programming//q/75401/781723を参照してください。ここでの一般的な用語は管理対象リソースです。この概念は、RAIIとファイナライザに基づいて構築されており、いくつかの点で改善されています。


迅速なリソースの割り当て解除にはあまり興味がなく、ジャストインタイムの割り当て解除という考え方にはもっと興味があります。RIAAはすばらしいですが、非常に多くのガベージコレクション言語にはあまり適用できません。Javaには、特定のリソースが不足する時期を知る機能がありません。ブラケット型の操作を使用すると便利で、エラーを処理できますが、私はそれらに興味がありません。私は単にリソースを割り当てたいだけで、それが都合のいいときや必要なときにいつでもクリーンアップされ、それを台無しにする方法はほとんどありません。誰もこれを実際に調べたことはないと思います。
マインドリーダー2016

2

すべてのメモリは等しいので、1Kを要求しても、アドレス空間で1Kがどこから来たかは気にしません。

ファイルハンドルを要求すると、開くファイルのハンドルが必要です。ファイルのファイルハンドルを開いていると、他のプロセスまたはマシンによるファイルへのアクセスがブロックされることがよくあります。

したがって、ファイルハンドルは必要がなくなったらすぐに閉じる必要があります。そうしないと、ファイルへの他のアクセスがブロックされますが、メモリが不足し始めたときにメモリを解放する必要があります。

GCパスの実行はコストがかかり、「必要な場合」にのみ実行されるため、プロセスが使用していない可能性があるファイルハンドルが別のプロセスで必要になる時期を予測することはできません。


あなたの答えは本当の鍵にあたります。メモリは代替可能であり、ほとんどのシステムには十分に再利用する必要がないため、すぐに再利用する必要はありません。対照的に、プログラムがファイルへの排他的アクセスを取得する場合、他のファイルがいくつ存在していても、そのファイルを使用する必要がある可能性のある他のプログラムをブロックします。
スーパーキャット2017年

0

これが他のリソースに対してあまりアプローチされなかった理由は、他のほとんどのリソースは誰でも再利用できるようにできるだけ早く解放されることが望ましいためです。

もちろん、例は既存のGCテクニックで「弱い」ファイル記述子を使用して提供できることに注意してください。


0

メモリにアクセスできなくなったかどうか(したがって、もう使用されないことが保証されているかどうか)を確認するのは簡単です。他のほとんどのタイプのリソースは、ほぼ同じ手法で処理できます(つまり、リソースの取得は初期化、RAII、およびユーザーが破棄されたときに解放され、メモリ管理にリンクされます)。ある種の「ジャストインタイム」解放を行うことは、一般的に不可能です(停止の問題を確認してください。あるリソースが最後に使用されたことを確認する必要があります)。はい、時々それは自動的に行うことができますが、それは記憶としてはるかに厄介なケースです。したがって、ほとんどの場合、ユーザーの介入に依存しています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.