C# Tips | ログ・例外・診断:GC情報取得

C# C#
スポンサーリンク

はじめに:GC情報取得は「.NET がどれくらい“片付け仕事”をしているか」を数字で見ること

C# の世界では、メモリの解放はガベージコレクタ(GC)が自動でやってくれます。
これはとても便利ですが、その分「今どれくらい GC が頑張っているのか」「GC が原因で重くなっていないか」が見えにくくなります。

そこで役に立つのが GC情報取得 です。
「どの世代の GC が何回走ったか」「今の GC モードは何か」といった情報を数字で取れるようにしておくと、

どのくらいオブジェクトが頻繁に生成・破棄されているか
GC がパフォーマンスに影響していそうか
メモリ周りのチューニングが必要かどうか

を判断する材料になります。

ここでは、初心者向けに

GC のざっくりした仕組み(世代という考え方)
GC.CollectionCount で「何回 GC が走ったか」を見る
GCSettings で GC モードを知る
ログと組み合わせた「GC情報取得ユーティリティ」

を、例題付きでかみ砕いて説明します。


GC のざっくりした仕組みをイメージする

世代(Generation)という考え方

.NET の GC は、メモリを「世代(Generation)」という単位で管理しています。

Generation 0(Gen 0)
生まれたばかりのオブジェクトが置かれる場所。
「すぐに使い捨てられるもの」が多い前提で、頻繁に、素早く回収される。

Generation 1(Gen 1)
Gen 0 の GC を生き残ったオブジェクトが昇格してくる場所。
「そこそこ長生きするかも」という中間層。

Generation 2(Gen 2)
さらに生き残り続けたオブジェクトが昇格してくる場所。
「長生きする前提」の領域で、GC の頻度は低いが、走るときのコストは大きい。

ざっくり言うと、「若い世代ほど頻繁に、軽く」「年寄り世代ほどたまに、重く」GC が走るイメージです。
この「各世代の GC が何回走ったか」を見るのが、GC.CollectionCount です。


GC.CollectionCount で「世代ごとの GC 回数」を見る

「Gen0 がめちゃくちゃ回っている」などが分かる

GC.CollectionCount(int generation) を使うと、「プロセス開始から今までに、その世代の GC が何回走ったか」が分かります。

public static void ShowGcCounts()
{
    int gen0 = GC.CollectionCount(0);
    int gen1 = GC.CollectionCount(1);
    int gen2 = GC.CollectionCount(2);

    Console.WriteLine($"GC 回数: Gen0={gen0}, Gen1={gen1}, Gen2={gen2}");
}
C#

例えば、アプリ起動直後にこれを呼ぶと、だいたい 0 かごく少ない数字です。
しばらく負荷をかけた後にもう一度呼ぶと、Gen0 が何百回、Gen1 が数十回、Gen2 が数回、といった数字になっていることが多いです。

ここでの重要ポイントは、「Gen0 が多いのは普通だが、Gen2 が頻繁に回っていると“重い GC が多い”可能性がある」ということです。
Gen2 の GC はコストが大きいので、短時間に何度も走っているようなら、メモリの使い方を見直すサインになります。


GCSettings で GC モードを知る

Workstation / Server、バックグラウンド GC など

System.Runtime 名前空間の GCSettings を使うと、今の GC の動作モードを知ることができます。

using System;
using System.Runtime;

public static void ShowGcSettings()
{
    Console.WriteLine($"IsServerGC: {GCSettings.IsServerGC}");
    Console.WriteLine($"LatencyMode: {GCSettings.LatencyMode}");
}
C#

IsServerGC は、「サーバー GC(マルチコア前提でスループット重視)かどうか」を表します。
ASP.NET やサービス系では true になることが多く、デスクトップアプリでは false(ワークステーション GC)が多いです。

LatencyMode は、「GC の“待ち時間”に関する方針」を表します。
通常は GCLatencyMode.Interactive ですが、リアルタイム性が重要な場面では一時的に LowLatency にする、といった使い方もあります。

初心者のうちは「今こういうモードで動いているんだな」と知るだけで十分ですが、
「サーバー GC かどうか」「LatencyMode が変な値になっていないか」をログに出しておくと、
本番環境の設定確認にも役立ちます。


GC情報をログに残すユーティリティを作る

「どのタイミングで GC がどれくらい走っているか」を後から追えるようにする

GC 情報は、その場で眺めるだけでなく、ログに残しておくと後から分析しやすくなります。
例えば、「バッチ処理の前後で GC 回数がどれくらい増えたか」を見る、といった使い方です。

ILogger と組み合わせたユーティリティの例です。

using System.Diagnostics;
using System.Runtime;
using Microsoft.Extensions.Logging;

public sealed class GcSnapshot
{
    public int Gen0Count { get; }
    public int Gen1Count { get; }
    public int Gen2Count { get; }

    public GcSnapshot()
    {
        Gen0Count = GC.CollectionCount(0);
        Gen1Count = GC.CollectionCount(1);
        Gen2Count = GC.CollectionCount(2);
    }

    public GcDiff DiffFrom(GcSnapshot before)
    {
        return new GcDiff(
            Gen0Count - before.Gen0Count,
            Gen1Count - before.Gen1Count,
            Gen2Count - before.Gen2Count);
    }
}

public readonly struct GcDiff
{
    public int Gen0Delta { get; }
    public int Gen1Delta { get; }
    public int Gen2Delta { get; }

    public GcDiff(int gen0Delta, int gen1Delta, int gen2Delta)
    {
        Gen0Delta = gen0Delta;
        Gen1Delta = gen1Delta;
        Gen2Delta = gen2Delta;
    }
}

public static class GcLogger
{
    public static GcSnapshot Capture() => new GcSnapshot();

    public static void LogCurrent(ILogger logger, string context)
    {
        var snap = new GcSnapshot();

        logger.LogInformation(
            "GCStatus Context={Context} Gen0={Gen0} Gen1={Gen1} Gen2={Gen2} IsServerGC={IsServerGC} LatencyMode={LatencyMode}",
            context,
            snap.Gen0Count,
            snap.Gen1Count,
            snap.Gen2Count,
            GCSettings.IsServerGC,
            GCSettings.LatencyMode);
    }

    public static void LogDiff(ILogger logger, string context, GcSnapshot before)
    {
        var after = new GcSnapshot();
        var diff = after.DiffFrom(before);

        logger.LogInformation(
            "GCDiff Context={Context} Gen0+={Gen0Delta} Gen1+={Gen1Delta} Gen2+={Gen2Delta}",
            context,
            diff.Gen0Delta,
            diff.Gen1Delta,
            diff.Gen2Delta);
    }
}
C#

使い方の例です。

// バッチ処理の前にスナップショットを取る
var before = GcLogger.Capture();

await RunBatchAsync();

GcLogger.LogDiff(_logger, "バッチ処理", before);
C#

ログには、例えばこんな行が残ります。

「GCDiff Context=バッチ処理 Gen0+=120 Gen1+=5 Gen2+=1」

ここでの重要ポイントは、「GC の“増分”を見る」ことです。
単に「今までに何回走ったか」だけでなく、「この処理の間に何回増えたか」を見ることで、
「この処理は Gen2 GC を頻繁に発生させている」といった判断ができるようになります。


実務での使いどころ:GC がボトルネックになっていないかを疑うとき

「CPU はそんなに使っていないのに、なんかカクつく」などのとき

GC 情報取得が特に役立つのは、次のような状況です。

CPU 使用率はそこまで高くないのに、アプリがカクつく
一定間隔で一瞬止まるような挙動がある
長時間動かしていると、だんだんレスポンスが悪くなる

こういうとき、「GC が頻繁に、しかも重い世代(Gen2)で走っていないか」を疑います。

バッチ処理や重い画面の前後で GcLogger を仕込んでおき、
「この処理の間に Gen2 が何回増えたか」を見ることで、
「オブジェクトの作りすぎ・長生きさせすぎ」がないかを考えるきっかけになります。

もちろん、最終的な深掘りにはプロファイラ(dotMemory など)を使うのがベストですが、
まずは「GC 周りが怪しいかどうか」をざっくり見るには、GC情報取得ユーティリティがちょうど良い入口になります。


まとめ:GC情報取得は“.NET の片付け仕事の様子”を数字で覗くためのユーティリティ

GC情報取得の本質を一言で言うと、

「.NET がどれくらい頻繁に、どの世代のメモリを片付けているかを数字で把握し、
パフォーマンス問題やメモリ問題の“匂い”を嗅ぎ分けるための材料にする」

ことです。

押さえておきたいポイントは次の通りです。

GC は世代(Gen0/1/2)でメモリを管理しており、若い世代ほど頻繁に、年寄り世代ほど重く回収される。
GC.CollectionCount で世代ごとの GC 回数を取得でき、Gen2 の頻発は要注意サインになり得る。
GCSettings.IsServerGCLatencyMode で、今の GC モードを知ることができる。
GC情報取得ユーティリティを作り、「処理の前後で GC 回数の増分をログに残す」と、どの処理が GC を多く発生させているかを後から分析できる。

ここまでイメージできていれば、「GC はよく分からない黒魔術」から一歩進んで、
“数字で語れる GC 観察”ができるようになります。

タイトルとURLをコピーしました