次の方法で共有


Windows システム上のラージ オブジェクト ヒープ

.NET ガベージ コレクター (GC) は、オブジェクトを小さいオブジェクトと大きなオブジェクトに分割します。 オブジェクトが大きい場合、その属性の一部は、オブジェクトが小さい場合よりも重要になります。 たとえば、圧縮する (つまり、ヒープ上の別の場所のメモリにコピーする) と、コストがかかる場合があります。 このため、ガベージ コレクターは、ラージ オブジェクト ヒープ (LOH) に大きなオブジェクトを配置します。 この記事では、オブジェクトを大きなオブジェクトとして修飾する対象、大きなオブジェクトを収集する方法、および大きなオブジェクトに与えるパフォーマンスへの影響について説明します。

Von Bedeutung

この記事では、Windows システムでのみ実行される .NET Framework と .NET Core のラージ オブジェクト ヒープについて説明します。 他のプラットフォーム上の .NET 実装で実行されている LOH については説明しません。

オブジェクトが LOH で終わる方法

オブジェクトのサイズが 85,000 バイト以上の場合は、大きなオブジェクトと見なされます。 この数は、パフォーマンスチューニングによって決定されました。 オブジェクト割り当て要求が 85,000 バイト以上の場合、ランタイムはラージ オブジェクト ヒープに割り当てます。

これが何を意味するのかを理解するには、ガベージ コレクターに関するいくつかの基礎を調べると便利です。

ガベージ コレクターは世代別コレクターです。 世代 0、第 1 世代、第 2 世代の 3 つの世代があります。 3 世代の理由は、適切に調整されたアプリでは、ほとんどのオブジェクトが gen0 で死ぬからです。 たとえば、サーバー アプリでは、各要求に関連付けられている割り当ては、要求が完了した後に終了する必要があります。 インフライト割り当て要求によって gen1 に変換され、そこで死ぬことになります。 基本的に、gen1 は、若いオブジェクト領域と有効期間の長いオブジェクト領域の間のバッファーとして機能します。

新しく割り当てられたオブジェクトにより、新しいオブジェクトが生成されます。また、新しく割り当てられたオブジェクトは暗黙的にジェネレーション 0 コレクションになります。 ただし、大きなオブジェクトの場合は、大きなオブジェクト ヒープ (LOH) に移動します。これは、ジェネレーション 3 と呼ばれることもあります。 ジェネレーション 3 は、ジェネレーション 2 の一部として論理的に収集される、物理的なジェネレーションです。

大きなオブジェクトはジェネレーション 2 コレクション中にのみ収集されるため、ジェネレーション 2 に属します。 世代が収集されると、その若い世代もすべて収集されます。 たとえば、ジェネレーション 1 の GC が発生すると、ジェネレーション 1 と 0 の両方が収集されます。 ジェネレーション 2 の GC が発生すると、ヒープ全体が収集されます。 このため、ジェネレーション 2 の GC は 完全な GC とも呼ばれます。 この記事では、完全な GC ではなく第 2 世代 GC を参照しますが、用語は交換可能です。

世代によって、GC ヒープの論理ビューが提供されます。 物理的には、オブジェクトはマネージド ヒープ セグメントに格納されます。 マネージド ヒープ セグメントは、GC がマネージド コードに代わって VirtualAlloc 関数を呼び出すことによって OS から予約するメモリのチャンクです。 CLR が読み込まれると、GC は 2 つの初期ヒープ セグメントを割り当てます。1 つは小さなオブジェクト (小さなオブジェクト ヒープまたは SOH) 用で、1 つはラージ オブジェクト (ラージ オブジェクト ヒープ) 用です。

その後、これらのマネージド ヒープ セグメントにマネージド オブジェクトを配置することで、割り当て要求が満たされます。 オブジェクトが 85,000 バイト未満の場合は、SOH のセグメントに配置されます。それ以外の場合は、LOH セグメントに配置されます。 セグメントは、より多くのオブジェクトがそれらに割り当てられるので(より小さなチャンクで)コミットされます。 SOH の場合、GC で存続するオブジェクトは次の世代に昇格されます。 ジェネレーション 0 のコレクションに残るオブジェクトは、ジェネレーション 1 のオブジェクトと見なされるようになりました。 ただし、最も古い世代に残るオブジェクトは、まだ最も古い世代にあると見なされます。 つまり、ジェネレーション 2 のサバイバーは第 2 世代オブジェクトです。および LOH からのサバイバーは LOH オブジェクトです (gen2 で収集されます)。

ユーザー コードは、第 0 世代 (小さなオブジェクト) または LOH (ラージ オブジェクト) でのみ割り当てることができます。 ジェネレーション 1 (ジェネレーション 0 からサバイバーを昇格) およびジェネレーション 2 (世代 1 から生存者を昇格) でオブジェクトを "割り当てる" ことができます。

ガベージ コレクションがトリガーされると、GC はライブ オブジェクトをトレースして圧縮します。 しかし、圧縮は高価であるため、GCはLOHを スイープ します。これにより、大きなオブジェクト割り当て要求を満たすために後で再利用できる、デッド オブジェクトから空きリストが作成されます。 隣接するデッド オブジェクトは、1 つのフリー オブジェクトに作成されます。

.NET Core と .NET Framework (.NET Framework 4.5.1 以降) には、次の完全ブロック GC 中に LOH を圧縮するようにユーザーが指定できる GCSettings.LargeObjectHeapCompactionMode プロパティが含まれています。 また、将来、.NET は LOH を自動的に圧縮することを決定する可能性があります。 つまり、大きなオブジェクトを割り当てて移動しないようにする場合は、引き続きピン留めする必要があります。

図 1 は、Obj2Obj3が停止している第 1 世代の GC の後に GC が第 1 世代を形成し、Obj2Obj5が停止している第 1 世代 GC の後に第 2 世代を形成するシナリオを示しています。 この図と次の図は、説明のみを目的としています。ヒープで何が起こるかを示すために、非常に少数のオブジェクトが含まれています。 実際には、多くのオブジェクトが通常 GC に関係しています。

図 1: Gen 0 GC と gen 1 GC
図 1: 世代 0 と世代 1 の GC。

図 2 は、 Obj1Obj2 が停止していることを確認した第 2 世代 GC の後、GC は、 Obj1Obj2によって占有されていたメモリの連続した空き領域を形成し、その後、 Obj4の割り当て要求を満たすために使用されたことを示しています。 セグメントの最後のオブジェクト ( Obj3) の後のスペースを使用して、割り当て要求を満たすこともできます。

図 2: Gen 2 GC の後
図 2: 第 2 世代 GC の後

大きなオブジェクト割り当て要求に対応できる十分な空き領域がない場合、GC は最初に OS からさらにセグメントを取得しようとします。 それが失敗した場合は、スペースを解放することを期待して第 2 世代 GC をトリガーします。

ジェネレーション 1 またはジェネレーション 2 の GC 中に、ガベージ コレクターは VirtualFree 関数を呼び出して、ライブ オブジェクトがないセグメントを OS に解放します。 セグメントの最後までの最後のライブ オブジェクトの後の領域はデコミットされます (ただし、gen0/gen1 が存在するエフェメラル セグメントでは、アプリケーションがすぐに割り当てられるため、ガベージ コレクターによってコミットが保持されます)。 また、空き領域はリセットされてもコミットされたままです。つまり、OS はディスクにデータを書き戻す必要はありません。

LOH は第 2 世代 GC の間にのみ収集されるため、LOH セグメントはそのような GC の間にのみ解放できます。 図 3 は、ガベージ コレクターが 1 つのセグメント (セグメント 2) を OS に解放し、残りのセグメントの領域を増やすシナリオを示しています。 大きなオブジェクト割り当て要求を満たすためにセグメントの末尾にあるデコミットされた領域を使用する必要がある場合は、メモリをもう一度コミットします。 (コミット/デコミットの説明については、 VirtualAlloc のドキュメントを参照してください)。

図 3: Gen 2 GC の後の LOH
図 3: 第 2 世代 GC の後の LOH

ラージ オブジェクトはいつ収集されますか?

一般に、GC は次の 3 つの条件のいずれかで発生します。

  • 割り当てがジェネレーション 0 またはラージ オブジェクトのしきい値を超えています。

    しきい値は、世代のプロパティです。 ガベージ コレクターがオブジェクトを割り当てると、世代のしきい値が設定されます。 しきい値を超えると、その世代で GC がトリガーされます。 小さいオブジェクトまたは大きなオブジェクトを割り当てる場合は、それぞれ第 0 世代と LOH のしきい値を使用します。 ガベージ コレクターがジェネレーション 1 とジェネレーション 2 に割り当てると、そのしきい値が消費されます。 これらのしきい値は、プログラムの実行時に動的に調整されます。

    これが一般的なケースです。ほとんどの GC は、マネージド ヒープでの割り当てが原因で発生します。

  • GC.Collect メソッドが呼び出されます。

    パラメーターなしの GC.Collect() メソッドが呼び出された場合、または別のオーバーロードが引数として GC.MaxGeneration 渡された場合、LOH はマネージド ヒープの残りの部分と共に収集されます。

  • システムのメモリが不足しています。

    これは、ガベージ コレクターが OS から高メモリ通知を受信したときに発生します。 ガベージ コレクターがジェネレーション 2 GC を実行すると生産性が向上すると考えられる場合は、1 つをトリガーします。

LOH パフォーマンスへの影響

大きなオブジェクト ヒープに対する割り当ては、次の方法でパフォーマンスに影響します。

  • 割り当てコスト。

    CLR は、提供するすべての新しいオブジェクトのメモリがクリアされることを保証します。 つまり、ラージ オブジェクトの割り当てコストは、(GC をトリガーしない限り) メモリクリアによって支配されます。 1 バイトをクリアするのに 2 サイクルかかる場合は、最小のラージ オブジェクトをクリアするのに 170,000 サイクルかかります。 2 GHz コンピューターで 16 MB のオブジェクトのメモリをクリアするには、約 16 ミリ秒かかります。 これはかなり大きなコストです。

  • 収集コスト。

    LOH とジェネレーション 2 は一緒に収集されるため、いずれかのしきい値を超えると、ジェネレーション 2 のコレクションがトリガーされます。 LOH が原因でジェネレーション 2 コレクションがトリガーされた場合、ジェネレーション 2 は必ずしも GC の後ではるかに小さいとは限りません。 第 2 世代のデータがあまりない場合、影響は最小限です。 ただし、ジェネレーション 2 が大きい場合は、多くの第 2 世代の PC がトリガーされると、パフォーマンスの問題が発生する可能性があります。 多数のラージ オブジェクトが一時的に割り当てられ、SOH が大きい場合は、PC の実行に時間がかかりすぎる可能性があります。 さらに、実際に大きなオブジェクトの割り当てと解放を続ける場合は、割り当てコストが実際に加算される可能性があります。

  • 参照型を持つ配列要素。

    LOH 上の非常に大きなオブジェクトは通常、配列です (非常に大きいインスタンス オブジェクトを持つことは非常にまれです)。 配列の要素が参照リッチである場合、要素が参照リッチでない場合は存在しないコストが発生します。 要素に参照が含まれていない場合、ガベージ コレクターは配列をまったく通過する必要はありません。 たとえば、配列を使用してノードをバイナリ ツリーに格納する場合、それを実装する 1 つの方法は、ノードの左右のノードを実際のノードで参照することです。

    class Node
    {
       Data d;
       Node left;
       Node right;
    };
    
    Node[] binary_tr = new Node [num_nodes];
    

    num_nodesが大きい場合、ガベージ コレクターは要素ごとに少なくとも 2 つの参照を通過する必要があります。 別の方法として、右ノードと左ノードのインデックスを格納します。

    class Node
    {
       Data d;
       uint left_index;
       uint right_index;
    } ;
    

    左側のノードのデータを left.dと呼ぶのではなく、 binary_tr[left_index].dと呼びます。 また、ガベージ コレクターは、左右のノードの参照を調める必要はありません。

3 つの要因のうち、最初の 2 つの要素は通常、3 番目よりも重要です。 このため、一時的なオブジェクトを割り当てるのではなく、再利用する大きなオブジェクトのプールを割り当てることをお勧めします。

LOH のパフォーマンス データを収集する

特定の領域のパフォーマンス データを収集する前に、既に次の作業を行っている必要があります。

  1. この領域を見ている必要があることを示す証拠が見つかりました。
  2. 見たパフォーマンスの問題を説明できるものを見つけずに、知っている他の領域を使い果たしました。

メモリと CPU の基礎の詳細については、解決策を見つけようとする前に、ブログ 「問題を理解する」を参照してください。

次のツールを使用して、LOH パフォーマンスに関するデータを収集できます。

.NET CLR メモリ パフォーマンス カウンター

.NET CLR メモリ パフォーマンス カウンターは、通常、パフォーマンスの問題を調査するための適切な最初の手順です ( ただし、ETW イベントを使用することをお勧めします)。 パフォーマンス カウンターを確認する一般的な方法は、パフォーマンス モニター (perfmon.exe) です。 [ 追加] (Ctrl + A) を選択して、関心のあるプロセスの興味深いカウンターを追加します。 パフォーマンス カウンター データをログ ファイルに保存できます。

.NET CLR メモリ カテゴリの次の 2 つのカウンターは、LOH に関連しています。

  • # Gen 2 コレクション

    プロセスの開始後にジェネレーション 2 の PC が発生した回数を表示します。 カウンターは、ジェネレーション 2 のコレクション (フル ガベージ コレクションとも呼ばれます) の終了時にインクリメントされます。 このカウンターには、最後に観察された値が表示されます。

  • 大きなオブジェクト ヒープ サイズ

    LOH の現在のサイズ (空き領域を含む) をバイト単位で表示します。 このカウンターは、各割り当てではなく、ガベージ コレクションの最後に更新されます。

パフォーマンス モニターでのカウンターの追加を示すスクリーンショット。

PerformanceCounter クラスを使用してプログラムでパフォーマンス カウンターにクエリを実行することもできます。 LOH の場合は、 CategoryName として ".NET CLR Memory" を指定し、 CounterNameとして "Large Object Heap size" を指定します。

PerformanceCounter performanceCounter = new()
{
    CategoryName = ".NET CLR Memory",
    CounterName = "Large Object Heap size",
    InstanceName = "<instance_name>"
};

Console.WriteLine(performanceCounter.NextValue());

通常、定期的なテスト プロセスの一環としてプログラムでカウンターを収集します。 通常とは別の値を持つカウンターを特定する場合は、他の手段を使用して、調査に役立つより詳細なデータを取得します。

ETW は、より豊富な情報を提供するため、パフォーマンス カウンターの代わりに ETW イベントを使用することをお勧めします。

ETW イベント

ガベージ コレクターには、ヒープの動作とその理由を理解するのに役立つ、豊富な ETW イベントのセットが用意されています。 次のブログ記事では、ETW を使用して GC イベントを収集して理解する方法を示します。

一時的な LOH 割り当てによって発生する過剰なジェネレーション 2 の VC を特定するには、VC の [トリガー理由] 列を参照してください。 一時的なラージ オブジェクトのみを割り当てる単純なテストでは、次の PerfView コマンドを使用して ETW イベントに関する情報を収集できます。

perfview /GCCollectOnly /AcceptEULA /nogui collect

結果は次のようになります。

PerfView の ETW イベントを示すスクリーンショット。

ご覧のように、すべての GC は第 2 世代の GC であり、それらはすべて AllocLarge によってトリガーされます。これは、大きなオブジェクトを割り当てることによってこの GC がトリガーされたことを意味します。 LOHの生存率 %列には1%が書かれているので、これらの割り当てが一時的であることを知っています。

これらのラージ オブジェクトを割り当てたユーザーを示す追加の ETW イベントを収集できます。 次のコマンド ライン:

perfview /GCOnly /AcceptEULA /nogui collect

は AllocationTick イベントを収集します。これは、約 100,000 分の割り当てごとに発生します。 つまり、大きなオブジェクトが割り当てられるたびにイベントが発生します。 その後、GC ヒープアロケーション ビューのいずれかを確認できます。このビューには、ラージ オブジェクトを割り当てた呼び出し履歴が表示されます。

ガベージ コレクター ヒープ ビューを示すスクリーンショット。

ご覧のように、これは、 Main メソッドから大きなオブジェクトを割り当てるだけの非常に単純なテストです。

デバッガー

メモリ ダンプだけで、実際に LOH 上にあるオブジェクトを確認する必要がある場合は、.NET によって提供される SoS デバッガー拡張機能 を使用できます。

このセクションで説明するデバッグ コマンドは、 Windows デバッガーに適用できます。

LOH の分析からの出力例を次に示します。

0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment   begin allocated     size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment   begin allocated     size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT   Count   TotalSize Class Name
001521d0       66     2081792     Free
7912273c       63     6663696 System.Byte[]
7912254c       4     8008736 System.Object[]
Total 133 objects

LOH ヒープ サイズは (16,754,224 + 16,699,288 + 16,284,504) = 49,738,016 バイトです。 アドレス 023e1000 から 033db630 の間では、8,008,736 バイトが System.Object オブジェクトの配列によって占有され、6,663,696 バイトが System.Byte オブジェクトの配列によって占有され、2,081,792 バイトが空き領域によって占有されます。

場合によっては、デバッガーで LOH の合計サイズが 85,000 バイト未満であることが示されることがあります。 これは、ランタイム自体が LOH を使用して、大きなオブジェクトよりも小さいオブジェクトを割り当てるために発生します。

LOH は圧縮されないため、LOH が断片化の原因と考えられる場合があります。 断片化とは、次のことを意味します。

  • マネージド ヒープの断片化。マネージド オブジェクト間の空き領域の量によって示されます。 SoS では、 !dumpheap –type Free コマンドはマネージド オブジェクト間の空き領域の量を表示します。

  • 仮想メモリ (VM) アドレス空間の断片化。これは、 MEM_FREEとしてマークされたメモリです。 これを取得するには、windbg のさまざまなデバッガー コマンドを使用します。

    次の例は、VM 空間の断片化を示しています。

    0:000> !address
    00000000 : 00000000 - 00010000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    00010000 : 00010000 - 00002000
    Type     00020000 MEM_PRIVATE
    Protect 00000004 PAGE_READWRITE
    State   00001000 MEM_COMMIT
    Usage   RegionUsageEnvironmentBlock
    00012000 : 00012000 - 0000e000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    … [omitted]
    -------------------- Usage SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Pct(Busy)   Usage
    701000 (   7172) : 00.34%   20.69%   : RegionUsageIsVAD
    7de15000 ( 2062420) : 98.35%   00.00%   : RegionUsageFree
    1452000 (   20808) : 00.99%   60.02%   : RegionUsageImage
    300000 (   3072) : 00.15%   08.86%   : RegionUsageStack
    3000 (     12) : 00.00%   00.03%   : RegionUsageTeb
    381000 (   3588) : 00.17%   10.35%   : RegionUsageHeap
    0 (       0) : 00.00%   00.00%   : RegionUsagePageHeap
    1000 (       4) : 00.00%   00.01%   : RegionUsagePeb
    1000 (       4) : 00.00%   00.01%   : RegionUsageProcessParametrs
    2000 (       8) : 00.00%   00.02%   : RegionUsageEnvironmentBlock
    Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
    
    -------------------- Type SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    7de15000 ( 2062420) : 98.35%   : <free>
    1452000 (   20808) : 00.99%   : MEM_IMAGE
    69f000 (   6780) : 00.32%   : MEM_MAPPED
    6ea000 (   7080) : 00.34%   : MEM_PRIVATE
    
    -------------------- State SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    1a58000 (   26976) : 01.29%   : MEM_COMMIT
    7de15000 ( 2062420) : 98.35%   : MEM_FREE
    783000 (   7692) : 00.37%   : MEM_RESERVE
    
    Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
    

ガベージ コレクターが OS から新しいマネージド ヒープ セグメントを頻繁に取得し、空のマネージド ヒープ セグメントを OS に解放する必要がある一時的なラージ オブジェクトによって VM の断片化が発生することがよくあります。

LOH が VM の断片化を引き起こしているかどうかを確認するには、 VirtualAllocVirtualFree にブレークポイントを設定して、それらを呼び出したユーザーを確認できます。 たとえば、OS から 8 MB を超える仮想メモリ チャンクを割り当てようとしたユーザーを確認するには、次のようにブレークポイントを設定します。

bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"

このコマンドは、割り当てサイズが 8 MB (0x800000) を超える VirtualAlloc が呼び出された場合にのみ、デバッガーに分割され、呼び出し履歴が表示されます。

CLR 2.0 では 、VM Hoarding と呼ばれる機能が追加されました。これは、セグメント (大きなオブジェクト ヒープと小さいオブジェクト ヒープを含む) が頻繁に取得およびリリースされるシナリオに役立ちます。 VM Hoarding を指定するには、ホスティング API を介して STARTUP_HOARD_GC_VM というスタートアップ フラグを指定します。 空のセグメントを OS に解放する代わりに、CLR はこれらのセグメントのメモリをデコミットし、スタンバイ リストに配置します。 (大きすぎるセグメントの場合、CLR ではこれを行いません)。CLR は後でこれらのセグメントを使用して、新しいセグメント要求を満たします。 次にアプリに新しいセグメントが必要な場合、CLR では、十分な大きさのセグメントが見つかると、このスタンバイ リストのセグメントが使用されます。

VM の保管は、メモリ不足の例外を回避するために、システム上で実行されている主要なアプリである一部のサーバー アプリなど、既に取得したセグメントを保持するアプリケーションにも役立ちます。

この機能を使用する場合は、アプリケーションのメモリ使用量が非常に安定していることを確認するために、アプリケーションを慎重にテストすることを強くお勧めします。