メモリリーク・リソースリークの検出手法
数時間の動作試験では問題がなかった機器が、数週間の連続稼働後にメモリ不足で停止することがあります。メモリリークは、組み込みシステムで発見が遅れやすい不具合のひとつです。
この記事では、組み込みならではのリークの特殊性と予防の考え方を押さえたうえで、実行時に検出する方法と、静的解析・長期エージングによる検証手法を順に説明します。
組み込みでのリークの特殊性と予防
組み込み機器は再起動の機会が少なく、長期間にわたって連続稼働することが多いという特徴があります。そのため、1回あたりのリーク(解放漏れ)がわずかであっても、稼働が続くうちに積み重なり、いずれメモリの確保に失敗します。
まずは何をリークと呼ぶのか、なぜ予防が先に来るのかを整理しましょう。
メモリリークとリソースリーク
メモリリークは、確保したメモリを不要になった後も解放しないこと(解放漏れ)で、その領域が再利用できなくなる状態を指します。一方でリークするのはメモリだけではありません。ファイルハンドル、ソケット、ミューテックスといった有限のリソースも、取得と解放が対になっていなければ同じように枯渇します。
ファイルをオープンした後にクローズを忘れれば、いずれ新しいファイルをオープンできなくなります。メモリ、ファイルハンドル、ソケット、ミューテックスなどのリソースは、取得の責務を持つモジュールが、不要になった時点で対応する解放処理を必ず実行するように設計します。
断片化による「見かけ上の枯渇」
メモリ不足による確保の失敗は、リークだけが原因とは限りません。確保と解放を繰り返すうちに、使用中の領域のあいだに小さな空きが飛び飛びに残り、空き容量の合計は足りているのに連続した領域が取れなくなることがあります。
この現象が断片化です。リークが「解放漏れによる純増」であるのに対し、断片化は「解放済みでも使えない」状態であり、両者は原因も対処も異なります。
このため、動的な確保・解放そのものをできるだけ避けることが有効です。固定長ブロックのメモリプールを採用すると、外部断片化を抑えやすくなります。さらに、空きブロックを連結リストやビットマップで管理し、探索を伴わない実装にすれば、確保・解放時間を一定に近づけやすくなります(ただし、返却漏れは別途監視が必要)。
予防が前提(動的確保回避・メモリプール・所有権)
検出手法の前に押さえたいのは、そもそもリークしにくい設計にするという発想です。具体的には次のような規律が定石になっています。
- 起動後の動的確保を避け、静的確保やスタック確保を優先する
- 動的な確保が必要な場合は、固定長ブロックを管理するメモリプールを使い、断片化を抑えやすくするとともに確保・解放の所要時間を一定化する。ただし、ブロックの返却漏れによるリークは起こり得るため、所有権の設計と監視は別途必要になる
- 固定長ブロック方式では、要求サイズより大きいブロックを使用することで内部断片化が発生する場合がある。また、プールごとにブロック数の上限があるため、最大同時使用数を見積もり、枯渇時の処理も設計しておく
- 確保した領域を誰が解放するのかという所有権を、モジュール間の取り決めとして明文化する
確保の経路を限定しておけば、万一リークしたときも調べる範囲が狭くて済みます。予防と検出は対立する選択肢ではなく、予防を敷いたうえで検出の網をかける関係です。
実行時に検出する
設計で予防しても、リークの可能性はゼロになりません。動作しているシステムから証拠を取る仕組みを用意します。
ヒープ使用量・最大使用量の常時監視
手軽で効果が大きいのは、ヒープ(プログラムの実行中に、必要なサイズの領域を動的に確保・解放するためのメモリ領域)の空き容量と、稼働開始からの最大使用量であるハイウォーターマークを定点観測することです。
一定周期で、現在のヒープ使用量、空き容量、稼働開始後の最小空き容量、および最大使用量であるハイウォーターマークを記録します。現在使用量や空き容量が同一条件下で継続的に悪化していれば、メモリリークなどによる継続的な消費が疑われます。一方、ハイウォーターマークは必要容量の見積もりや最大負荷時の余裕確認に活用します。
逆に、増加が初期化直後だけで頭打ちになり、その後の動作で安定していれば、リークが継続する可能性は低いと考えられます。この監視を製品に組み込んでおけば、市場での長期稼働時にもメモリ使用量の変化を追跡できます。
製品版では、現在使用量、最小空き容量、確保失敗回数などの軽量な統計値を記録します。確保履歴や呼び出し元情報などの詳細な追跡は、開発版・診断モード・障害解析用ビルドで有効化する設計が現実的です。
確保・解放のフックによる追跡とホスト環境の検出ツールの活用
リークの存在がわかったら、次は場所の特定です。確保・解放を追跡する場合は、メモリアロケータの呼び出し窓口をラッパー関数やメモリプールAPIに集約します。あわせて、ラッパーマクロまたは専用の確保APIを用い、確保サイズ、呼び出し元のファイル名・行番号、必要に応じてタスク名や時刻を記録します。
ライブラリ内部やRTOS内部の確保まで追跡対象にするかは、使用するOS、ランタイムライブラリ、計測方式に応じて事前に確認します。
これらの情報ごとに確保と解放の収支を集計すれば、解放漏れが発生している箇所が浮かび上がります。
また、ハードウェア依存の少ないロジック部分はホストPC上でビルドし、動的解析ツールで検査する方法が有効です。実機では捉えにくいリークや不正なメモリアクセス(範囲外アクセスなど)を、豊富なメモリと解析能力のある環境で洗い出せます。
ただし、ターゲットとホストではコンパイラ、データ型のサイズ、アラインメント、RTOSの動作条件などが異なることがあります。そのため、ホストでの精密検査に加え、実機でも軽量な使用量監視と長期試験をおこなうことが重要です。
静的解析・単体テストと長期エージングによる検証
実行時の検出には、その経路が実際に実行されなければ見つからないという限界があります。実行前の検査と長期試験で網を重ねます。
長期エージングでは、通常動作だけでなく、通信切断・再接続、タイムアウト、入力異常、デバイス抜き差し、処理失敗からの復旧など、実運用で起こり得る異常系のコードパスも繰り返し実行します。
静的解析・単体テストで早期に防ぐ
静的解析ツールは、コードを実行することなくリークの経路を洗い出します。特に、エラー発生時にreturnを使って抜ける経路で解放処理が実行されないパターンは、人の目では見落としやすく、静的解析が得意とする領域です。
ただし、静的解析だけではすべてのリークを検出できないという限界も理解しておく必要があります。単体テストでは、初期化完了後の使用量をベースラインとして定義し、各テストの実行後にメモリ使用量やリソース数が期待する状態へ戻ることを確認します。常駐オブジェクトやキャッシュなど、意図的に保持するリソースは、あらかじめ除外条件として明文化します。この収支チェックを自動テストに組み込んでおけば、修正や機能追加によるリークの再発を早期に検出できます。
長期エージングとリーク箇所の特定
出荷前の最後の網が、連続稼働試験によるエージングです。実運用に近い負荷をかけながら長時間動かし、ヒープ使用量の傾きを観察します。
ゆっくり進行するリークは短時間の試験ではほぼ顕在化しないため、時間をかけた試験でしか検出できません。傾きが確認できたら、負荷の種類を切り換えて傾きの変化を見ることで、どの機能にリークがあるかを絞り込めます。
見落とされやすいのは、エラー処理のコードパスです。正常系では十分にテストされないことが多いため、こうした経路も重点的に点検し、確保と解放の対応を確かめることが恒久対策につながります。


