雑誌のテーマからプロジェクト実践へ
該当記事に関連するサービス・技術ページ
稼働中の Delphi アプリケーションが徐々に「膨張」し、断続的に Access Violations で落ちる、あるいは数日稼働した後に突然不安定になる場合、背後にあるのは単一のバグではなくパターンであることが多い。メモリが割り当てられるがきちんと解放されない、あるいは早すぎて解放され、その後にまだ使用される。まさにこの局面で FastMM FullDebugMode は有効だ。常時有効にするためではなく、「ヒープのどこかが壊れている」という状態から再現可能な原因を特定するための診断ツールとして使うべきである。
問題点は、FullDebugMode が大量の出力を生成し、パフォーマンスに影響を与え、誤解を招きやすい点にある。リークレポートが自動的に「エラーのある場所」を示すわけではないこと、スタックトレースはシンボル解決(MAP ファイル、デバッグ情報、インライン展開)次第でしかないことに注意が必要だ。本稿では典型的な周辺ケースを辿り、適切なアプローチと落とし穴を説明する — 最終的にリークを見つけるだけでなく、それを持続的に止められるようにすることを目的とする。
FastMM FullDebugMode が本当に有効なケース
FastMM は現代の Delphi バージョンで既定のメモリマネージャになっていることが多く、多くのプロジェクトで既に組み込まれている場合がある。しかし FullDebugMode は特別な設定である:メモリブロックに追加の検査パターンを付与し、割り当て時のスタックトレースを収集し、ヒープの破損(例えばバッファオーバーランによるヒープ管理データの破損)に対してより厳密にチェックする。
私は次のいずれかの状況があるときに FullDebugMode を目的を絞って使用する:
- 再現可能なリーク: テスト実行で操作ごとにメモリ使用量が増加する場合(例:リクエスト毎、インポート毎、UI 操作毎)。
- 断続的な AV: 特に同じ領域で「ここでもあそこでも」と発生するようなもの(典型例:Use-after-free)。
- ヒープ破損: 「Invalid pointer operation」や「Access violation in ntdll」といった報告、あるいは終了/ファイナライズ時のクラッシュ。
- 回帰の追跡: リファクタリング、ライブラリ更新、コンパイラ変更の後に突然新しい不安定性が出た場合。
FullDebugMode を「すべてのビルドで一律に有効にする」のは適切ではない。オーバーヘッドが大きく、タイミングが変わるため、特にレースコンディションは消えたり別の場所に移動したりする。長期稼働にはプロセスのワーキングセットや Private Bytes、操作ごとのカウンタなどの軽量な監視が向いている — FullDebugMode は診断用のメスであり、常時稼働の脈拍センサーではない。
Grundprinzip: Leak-Report ist Symptom, Stacktrace ist Spur
リークレポートはまず 「これらのブロックがプログラム終了時にまだ割り当てられている」 ことを示す。これが自動的に問題になるのは、それらのブロックが本来解放されるべきであった場合のみである。正当な「リーク」も存在する:グローバルなシングルトン、キャッシュ、プロセスライフタイムを持つ OS ハンドル、あるいは意図的にファイナライズしないサードパーティライブラリなどだ。これらは把握しておくべきだが、盲目的に「修正」してはいけない。
レポートのスタックトレースはブロックが割り当てられた位置を示す。多くの場合それは「Free を忘れた」場所ではない。成長したシステムでよくある現実は次の通りである:
- UI レイヤーやサービスレイヤーでの割り当てだが、解放はより下位の層で行われるべきであり、所有権が不明確である。
- Factory で割り当てを行い所有権をコーラーに渡す設計だが、コーラーがそれを自分の所有物(“owned“)だと誤認している。
- オブジェクトがコレクション(リスト、ディクショナリ)で保持されるが、所有権モデルが一貫していない。
- 例外経路がクリーンアップをスキップする。try/finally が存在しないか、遅すぎて不十分である。
したがって明確な手順は次のとおりです: 再現 → 分離 → スタックトレースを解決 → 所有権(Ownership)エラーを特定 → 回帰テストを伴う修正。FastMM は痕跡を提供しますが、それをアーキテクチャとライフサイクルに照らして解釈する必要があります。
FastMM FullDebugMode を適切に有効化する(副作用を見落とさない)
実務では FullDebugMode は FastMM のオプションと適切な FastMM 設定によって有効化されます。重要なのは「インクルードファイルの正確な名前」ではなく、設定が何を実現するかと、どのビルド条件でそれを利用するかです。
デバッグビルドの推奨前提条件
- Debug DCU とデバッグ情報: スタックトレースは、実際のユニット/行/アドレスに解決できる場合にのみ有用です。デバッグ情報が生成され、MAPファイルが利用可能であることを確認してください。
- 最適化は意図的に選ぶ: スタックトレースの可読性のためには、非最適化ビルドの方が望ましい場合が多いです。インライン化や攻撃的な最適化はスタックフレームを「ぼかす」可能性があります。
- 同一の実行条件: 可能な限り同じデータ、同じ設定、同じ権限で実行してください。多くのリークはデータ依存です(例: 稀なフォーマット、特殊なパス)。
- 64-bit と 32-bit を分ける: メモリ挙動、アラインメント、サードパーティライブラリが異なります。問題が発生する対象プラットフォームでデバッグしてください。
管理者や技術責任者が見落としがちな点の一つは、FullDebugMode がタイミングにも影響を与える可能性があることです。スレッド処理が絡む場合、レースコンディションの発生が変わることがあります。したがって、再現を確認するだけの FullDebugMode なしの実行を並行して行うことが有用です。FullDebugMode は診断のためのステップです。
「ReportMemoryLeaksOnShutdown」に注意
Delphi は ReportMemoryLeaksOnShutdown を通じてプログラム終了時にリークを報告できます。これは便利ですが、複雑なアプリケーション(サービス、プラグインホスト、長時間稼働)では誤解を招くことがあります: シャットダウン時にはファイナライゼーション処理が走り、スレッドが停止し、キャッシュが解放されます。実行途中で致命的だったリークが終了時までに消えることもあれば、その逆に、バックグラウンド処理が残っているために終了時にのみ現れる見かけ上のリークが発生することもあります。
実務的なリーク検出では、重要なのは 操作ごとのリークを計測する ことです(例: 100リクエスト後など)。FastMM はその支援になりますが、テストの構成がそれを反映している必要があります。
典型的な境界ケース: リークレポートが「何らかのオブジェクト」を示すが、原因は所有権(Ownership)である
企業向けアプリケーションの古典的な問題: インポートプロセスがレコードごとに補助オブジェクト(例:StringLists、JSON-Parser、テンポラリリスト)を生成する。正常系では適切に解放されるが、稀なケース(バリデーションによるスキップ、例外、早期終了)でオブジェクトが残る。10.000件のレコード処理後に可視化される。
FastMM FullDebugModeはここで役立つ。なぜなら割り当て箇所を示してくれるからだ。しかし「Fix」は「割り当て箇所で free すること」ではない。Fixは堅牢な所有権パターンである:
- オブジェクトを生成した者が自動的にオーナーになるわけではない。
- 所有権はAPI契約で明確にする必要がある(パラメータ/戻り値、ドキュメント、命名規約)。
- コレクションは明確でなければならない:所有(owning)対非所有(non-owning)。混在形は後で祟る。
- 例外パスには早めの try/finally ブロックが必要である。
スタックトレースから「TStringList.Create」しか見えない場合、その情報は無価値ではない — しかしそれは単に「ここで何かが生成されている」と言っているに過ぎない。問題は:どこで終わるべきか? その点ではアーキテクチャ的思考がデバッガの曲芸よりも役に立つ。
Stacktraces korrekt lesen: Was du wirklich daraus ableiten kannst
FastMMからのスタックトレースは通常、復帰アドレスのリストであり、デバッグシンボルがあればユニット、プロシージャ、理想的には行番号にマップされる。これを読む際に重要なのは三つの点である:
- Top-of-Stackが常にエラーとは限らない:上位のフレームはしばしばメモリマネージャやRTLである。興味深いのは自分のコードが始まる箇所だ。
- 単一行ではなくコールチェーン:その行は一点に過ぎない。チェーンが、どの経路で割り当てが行われたかを示す。
- 同一のブロックが複数存在する場合:FastMMが複数の同サイズのリークを報告する場合、それは再現可能な経路であることが多い。これは好都合だ:再現性がある。
Wenn Zeilennummern fehlen: MAP-Datei, Packages, Release-DCUs
多くのチームがここで躓く:FullDebugModeは有効で、リークレポートは出るが、ユニット/行番号の代わりにアドレスや不明瞭なシンボルしかない。典型的な原因:
- MAPファイルがない、あるいはデバッグ情報が生成されていない。
- Release-DCUsやシンボルのないサードパーティDLLと対峙している。
- アプリケーションがRuntime Packagesを利用している:その場合コードの一部はBPLにあり、シンボル解決がそれに合わせて行われる必要がある。
- 最適化やインライン化によりスタックトレースが読みづらくなっている。
実務的には:リーク捜索には意図的に「診断可能」なビルドが必要である。これは「できるだけ速くする」目的とは異なる。技術的リードはこれを専用のビルドプロファイルとして扱い、チームメンバーがその都度プロジェクトオプションを変更しないようにすべきだ。
Frames bewerten: „Interessant“ ist oft eine Zeile weiter oben
実例(具体的な顧客コードは含まない):スタックトレースはコード内の最初のフレームとして「LoadConfig」ルーチンを示す。そこでオブジェクト生成が見える。Free を追加するとリークは消える――しかし別の箇所で突然二重解放(Double Free)でクラッシュする。なぜか?「LoadConfig」がそのオブジェクトをキャッシュに入れており、別のコード経路が既にオーナーになっていて後で片付けるためだ。
正しい読み方はこうだ:スタックトレースは、ブロックがどこで生成されたか を示している。修正は多くの場合定義にある:リターン後に誰がそのオブジェクトを所有するのか? この問いにきちんと答えないと、単に障害の様相が変わるだけ(メモリリーク → AV)。
Heap-Korruption vs. Leak: Warum FullDebugMode oft den echten Übeltäter findet
多くの「リーク」は実際には派生的な問題だ:バッファオーバーランがヒープメタデータを書き換え、メモリマネージャが後で正しく解放できなくなり、最終的に一見ランダムなリークや無効なポインタ操作が現れる。FullDebugMode は検査パターンを使い、Free/Reuse 時に追加の検証を行うため、ここで有効だ。
重要なのは区別である:
- メモリリーク: ブロックが割り当てられたまま解放されない。時間経過で安定性が損なわれるが、必ずしもクラッシュするとは限らない。
- Use-after-free: ブロックは解放されるが、その後でまだ使用される。再現が難しい突発的な AV を引き起こす。
- 二重解放: ブロックが二度解放される。即座にクラッシュすることもあれば(ブロックが再利用されるまで)後で問題が出ることもある。
- ヒープ破損: どこかがブロックの境界を越えて書き込む。症状はしばしば時間差で現れる。
症状が時間差で現れる場合、FullDebugMode は特に有用だ。追加の検証によりエラーを早期に可視化できる――多くの場合、誤ったアクセスが発生した箇所で検出され、数分後の任意の Free で表面化するのを防げる。
Vorgehen in Projekten: Reproduzierbare Leak-Jagd statt „Debugging im Nebel“
メモリリークを追跡するには、再現可能でチーム内で共有できる手順が必要だ。私は固定した診断フレームを用いることを好む:
1) Reproduktion in einem deterministischen Szenario
リークを確実に示すテストシーケンスを定める:「サービスを起動し、500件のメッセージを処理してサービスを停止する」や「画面 X を開き、操作 Y を200回実行する」など。重要なのは、データセット、Mandant、機能フラグといったパラメータを添えてシーケンスを文書化し、他者が同じ手順を再現できるようにすることだ。
2) Minimieren: Leak pro Schritt sichtbar machen
シーケンスが20分かかるなら分割しなさい。目標は「前」と「後」をできるだけ短時間で比較することだ。大規模なアプリケーションでは、修正作業そのものよりもこの分割と比較に時間を取られることが多い。
3) FullDebugMode einschalten und Report interpretieren
ここで初めて FastMM FullDebugMode の出番です。レポートを収集し、ブロックサイズ/コールスタックでグループ化して繰り返しを確認します。単一の残存ブロックは正当なキャッシュである可能性があります。10,000個の同一ブロックはほぼ確実に実際のリークです。
4) 所有権の明確化と適切なレイヤでの修正
所有権が定義される場所でリークを修正します: Factory、API契約、Collection-Wrapper。オブジェクトが渡される場合、Create の直後に「とりあえず Free を入れる」ことはしばしば誤りです。
5) リグレッション: 同じシーケンス、同じビルド、同じレポート
修正が確実なのは、同じシーケンスが再実行され、リークも新たなメモリエラーも発生しないときです。特に Use-after-free の場合、「リークが消えた」ことは証拠ではなく、新たな症状に過ぎません。
Delphi コードにおいて FastMM が可視化する典型的な落とし穴
コレクションと所有権(リスト、ディクショナリ、インターフェース)
多くのリークは複雑なアルゴリズムではなく、日常的なデータ構造に起因します。代表的な2つの誤り:
- リストがオブジェクトを保持しているが、誰がそれを解放するかが不明。対策: owning リストを使用するか、finally 内で確実にクリアする。
- ディクショナリが値としてオブジェクトを保持している; Remove 時に値が解放されない、または Clear で忘れられる。
さらに厄介なのが Interfaces です。参照カウント(ARC類似)は扱いやすい一方で、オブジェクト所有権と混在する運用は、循環参照やイベントによるリークを生むことがあります。FullDebugMode はしばしば割り当てパスを示しますが、原因は参照サイクル(A が Interface 経由で B を保持し、B がコールバックで A を保持する)の場合があります。
例外と早期終了
成熟した業務系ソフトウェアでは、例外が通常の制御フロー(例: バリデーション、中断、リトライ)の一部であることが多いです。問題は例外自体ではなく、その周辺の経路にあります: オブジェクトが try/finally の外側で生成され、例外によってクリーンアップがスキップされることがあります。FullDebugMode は割り当てのスタックトレースを提供するので、確実に実行される解放パスが存在するかを確認する必要があります。
スレッドとライフタイム: „誤ったスレッドでの解放“
VCL/FMX やワーカースレッドを伴うサービスでは別の境界事例が発生します: オブジェクトがあるスレッドで生成され、Queue/Synchronize で「ちょっと渡す」ために UI スレッドで解放される(またはその逆)ことがあります。これは動作する場合もありますが、Producer が作業を続けている間に Consumer が既に解放していると Use-after-free を引き起こす可能性があります。
ここで FastMM FullDebugMode が役立つ場合があります。時差のあるエラーを早期に検知できるからです。ただし根本的な修正はきちんとしたライフタイムモデルです: 明確な所有権、イミュータブルなデータのみでの受け渡し、または明確な所有権移譲ポイント。
レポートを実用化する方法: フィルタ、比較、記録
チームではリークレポートを単に「確認する」だけでなく、アーティファクトとして扱う価値があります。実績のある実用的な手法を3つ挙げます:
- Baseline-Report: 「既知の状態」(例: 現行プロダクトバージョン)を一度 FullDebugMode で実行し、参照として保管します。これにより新しいリークを即座に検出できます。
- Use-Caseごとの比較: 重要なワークフロー(インポート、エクスポート、API リクエスト、UIの大量操作)ごとに、定期的に再現可能な短いシーケンスを定義します。
- ドキュメント化された「正当なリーク」: キャッシュを意図的に最終化しない場合はその旨を記録してください。さもなければ6ヶ月後に誰かが同じ問題を追いかけます。
これは官僚的な手続きではなく時間の節約です。リーク追跡はさもないとすぐに終わりのない繰り返しになってしまいます。なぜなら同じパターンが各スプリントで再発するからです。
どのような場合に手間をかける価値があるか — そしていつ別の手法を採るべきか
FastMM FullDebugMode はコストを伴う診断ツールです。次の場合に特に手間をかける価値があります:
- アプリケーションが長時間稼働する(サービス、ターミナルサーバークライアント、シフト運用、24/7 プロセスなど)。
- テストで全てのパスを網羅できず、実際の顧客データフローを処理している場合。
- 短期的な機能速度よりも安定性を重視する場合(プロセスに近いソフトウェアソリューションで典型的)。
一方で、30秒ほどで終了する小さなデスクトップヘルパーだけであれば、リーク追跡は多くの場合二次的です。同様に、一度だけ発生するメモリスパイク(例:大規模なエクスポート)の場合は、しばしばリークではなくストリーミング戦略やヒープのピーク負荷の問題です。
実務上の結論:FullDebugMode はスイッチではなくプロセスである
FastMM FullDebugMode はメモリエラー探索に構造をもたらします:割り当てを可視化し、ヒープ破損を早期に検出し、原因を特定できるスタックトレースを提供することで、症状ではなく原因を修正できます。しかし決定的な効果を生むのはツール自体ではなくワークフローです:再現可能なシナリオ、診断可能なビルド、明確な所有権の取り決め、ベースラインに対する回帰チェック。
もし根深いメモリリークや断続的なヒープエラーで行き詰まっており、より大きな Delphi システム内でこの問題を持続的に安定化させたいのであれば、明確な手順と解析可能なレポートを備えた短期間の整った診断セットアップに投資する価値があります。解析、ビルドプロファイル、アーキテクチャのリファクタリングで支援が必要な場合は、Net-Base Software GmbH にお問い合わせください。
このテーマに関しては、Delphi における「メモリリークの発見」と「FastMM Leak Report の読み方」も重要です。本稿はこれらの側面を整理して日常作業で何に注意すべきかを示します。
次のステップ
テーマが実際のプロジェクトになる場合、アーキテクチャ、既存資産、運用は早期に一体として検討する必要があります。
私たちは単なる個別の問い合わせへの対応にとどまらず、ソースの断片やレガシー課題、ポータルの構想が堅牢な企業向けプロジェクトへと成長する段階まで支援します。
- 既存環境、目標像、技術的リスクを一体として評価します。
- REST、データアクセス、ポータル、およびロールアウトは後回しにされません。
- 早い段階で、どの選択肢が経済的かつ運用上実行可能であるかを見極められます。