はじめに
- プログラムを動かしたときに途中停止してしまう、異常終了しているという現象が起きた。
- 異常終了時のメトリックの状況を見ると、メモリ使用率が90%ほどになっていたことがわかった。
- 「どこでメモリを使用しているのか」を調べるために、プログラムの各動作の切れ目でのメモリ使用状況がどのようになっているのかをログで出力し、状況を特定しようとした。
- このとき、「物理メモリだけじゃ状況わからないからいろいろ見ようね」と言われて詰まった。これが今回の話につながる。
メモリの話
メモリというのはどういうもの?
- コンピューター処理に使うデータやプログラムを一時的に格納する場所
メモリと一口にいっても
- コンピューターは実際に搭載している「物理メモリ」に加えて、「仮想メモリ(ページメモリ)」というのを用意している。
- 物理メモリがいっぱいになるとコンピューターは動けなくなるので、仮想メモリはストレージを一部つかって予備メモリとして動かせるようにしている。
- ただし、ストレージは読み書きが遅いので、仮想メモリを使う状況になると処理がめちゃくちゃ遅くなる。これがページアウト。
ガベージコレクション
- 物理メモリと仮想メモリの状況だけでは、処理落ちの原因はわからない。アプリケーション実行によりメモリがどんな感じで確保されてどんな感じで解放されていったのか知る必要がある。
- 一時的にメモリがぐん!と上がっているというのなら一つめちゃくちゃ重い動作があってそれのせいでメモリが枯渇しているということかもしれないが、だんだん使用率があがっている、ということであればガベージコレクションが間に合っていないのかもしれない。
- ガベージコレクターというのは、メモリの管理を自動で行ってくれる機能。使用したいときにアドレス空間を予約して、使用しなくなったらメモリを自動で解放してくれる。C言語とかだとメモリ確保解放を明示的にしなければいけないが、C#はこのあたり自動でやってくれる。
- ガベージコレクターのアルゴリズム
- https://docs.microsoft.com/ja-jp/dotnet/standard/garbage-collection/fundamentals#memory-release
- ガベージコレクターのパフォーマンスを最適化するため、C#においてマネージドヒープはオブジェクトの有効期間に応じてG0、G1、G2の3つの世代に分けられる。
- マネージドヒープ:アプリケーション用に確保されたメモリの仮想アドレス空間
- 各世代の詳細は以下のとおり
- G0
- 新しいオブジェクトができたときに最初に格納される場所。有効期間が短いオブジェクトが格納される。
- なお、85KB以上の大きなオブジェクトは別途大きなオブジェクトヒープ(LOH)に移され、G2の一部として論理的に収集される。
- 一番よくGCされる。
- G0でごみでないと判断されたものは、G1に昇格される。
- 新しいオブジェクトができたときに最初に格納される場所。有効期間が短いオブジェクトが格納される。
- G1
- G0とG2とのバッファー的な役割を果たす。
- G0のガベージコレクションでメモリ確保が間に合わなかった場合、G1のガベージコレクションを行う。
- G1でごみでないと判断されたものは、G2に昇格される。
- G2
- 有効期間が長いオブジェクトが格納される。最後の砦。
- その後のコレクションで到達不能であると判断されるまで、G2に残る。
- G0
- ガベージコレクションが間に合っていない、という状況だとして、どの世代にどれくらい物がたまっているかにより対応策がかわってくる。