メモリリークは最も厄介なバグの1つです。アプリが遅くなり、ファンが回り始め、どのプロセスが原因なのか、なぜ起きているのかがわかりません。このガイドでは、ProcXrayを使ってmacOS上のメモリリークを見つけるための体系的なアプローチを紹介します。
結論から言うと
macOSでメモリリークをデバッグするには、まず回復しない持続的なメモリ増加を確認し、次にリークしているプロセスを特定し、起動コンテキストと環境変数を検証し、ファイルディスクリプターやライブラリの情報と相関させます。迅速なトリアージにはProcXrayを使い、コードレベルの根本原因にはプラットフォームのプロファイラーを使いましょう。
症状を確認する
ツールに手を伸ばす前に、正当な高メモリ使用ではなくメモリリークであることを確認しましょう:
- メモリが時間とともに着実に増加する(タスク中の一時的なスパイクではない)
- アプリがアイドル状態になってもメモリが減少しない
- 他のアプリを閉じてもシステムのスワップ使用量が増え続ける
- メモリ増加を引き起こした処理が完了してもプロセスがメモリを解放しない
ステップ1:リークしているプロセスを特定する
ProcXrayを開き、Memory(降順)でソートします。以下を探してください:
- メモリ欄の数値が上がり続けているプロセス
- そのプロセスの用途にしては異常に高いメモリ使用量(例:バックグラウンドデーモンが2GB使用)
ProcXrayのサイドバーにあるリアルタイムメモリチャートは継続的に更新されます。疑わしいプロセスをピン留めしてトレンドラインを監視しましょう。リークは、決して水平にならない右肩上がりの線として現れます。
ステップ2:プロセスの正体を確認する
疑わしいプロセスをクリックします。Generalタブには以下が表示されます:
- フルの実行ファイルパス(
nodeやpython3のようにプロセス名が曖昧な場合に便利) - コマンドライン引数(どのスクリプトやサーバーが動作しているかがわかる)
- ワーキングディレクトリ
- 親プロセス(何が起動したかがわかる)
3GBを消費しているバックグラウンドのnodeプロセスも、それがnode /Users/me/myapp/server.jsだとわかって初めて意味があります。
ステップ3:環境変数を確認する
Environmentタブに切り替えます。Node.js、Python、Rubyアプリのメモリリークは、環境設定のミスが原因であることがあります:
NODE_ENV=developmentの設定により、本番環境で重いデバッグミドルウェアが有効になっている- ORMが詳細ログを有効にしており、クエリオブジェクトがメモリに保持され続けている
- キャッシュライブラリがエビクションポリシーなしで設定されている
環境変数をJSONとしてコピーし、キャッシュサイズ、プールの上限、デバッグフラグを確認しましょう。
ステップ4:オープンファイルディスクリプターを確認する
サーバーのメモリリークは、ファイルディスクリプターのリークと連動していることが多いです。プロセスがファイル、ソケット、パイプを開いたまま閉じないのです。ProcXrayのConnectionsタブに切り替えると、以下が表示されます:
- すべてのオープンファイル
- すべてのリスニングポート
- すべてのアクティブなネットワーク接続
本来50個程度であるべきファイルディスクリプターが10,000個開いているサーバーは、関連するリソースリークの強いシグナルです。
ステップ5:読み込まれたライブラリを確認する
リークがネイティブライブラリに起因していることもあります。ProcXrayのModulesタブでは、プロセスが読み込んだすべての動的ライブラリが一覧表示されます。アプリの既知の依存関係と照らし合わせてください。予期しないライブラリバージョンや、存在すべきでないライブラリが見つかれば、異常な動作の説明になるかもしれません。
ステップ6:再現して確認する
容疑者(特定のプロセス+コードパス)を絞り込んだら、意図的にリークを再現します:
- ProcXrayでプロセスの現在のメモリ使用量を記録
- リークの原因と思われる操作を実行(例:ファイルのアップロード、クエリの実行、APIの呼び出し)
- ProcXrayでメモリのトレンドを監視
- アプリがアイドル状態になるのを待つ
- メモリがベースラインに戻らなければ、リークパスが確認できたことになります
ステップ7:プラットフォームツールで深掘りする
ProcXrayは「何が」「どこで」を教えてくれます。コード内の「なぜ」については、プラットフォーム固有のツールを使いましょう:
ネイティブSwift/Objective-Cアプリの場合:
leaks <PID> # Apple内蔵のリーク検出ツール
malloc_history <PID> # アロケーション履歴
またはXcodeのMemory Graph Debuggerを使ってビジュアルなコールツリーを確認できます。
Node.jsの場合:
node --inspect server.js
# Chrome DevToolsのMemoryタブまたはclinic.jsを使用
Pythonの場合:
from memory_profiler import profile
@profile
def my_function():
...
よくある原因
| 原因 | シグナル |
|---|---|
| 無制限のキャッシュ | リクエスト数に比例してメモリが増加 |
| 削除されていないイベントリスナー | UI操作に伴いメモリが増加 |
| ネイティブライブラリのリーク | Modulesタブでバージョンの不一致を確認 |
| 閉じられていないファイルディスクリプター | Connectionsタブで数千のオープンファイルを確認 |
| 保持されたDOMノード | ページ遷移してもブラウザプロセスのメモリが増大 |
まとめ
- ProcXrayでメモリ順にソートし、増加しているプロセスを特定
- Generalタブでプロセスの正体と起動方法を確認
- 環境変数の設定ミスがないか確認
- Connectionsタブでファイルディスクリプターのリークを確認
- リークパスを再現
- Xcode Memory Debugger、clinic.js、memory_profilerでコードレベルの分析に引き継ぎ
ProcXrayをダウンロード → して調査を開始 — 無料、macOS Sonoma以降対応。
FAQ
メモリリークと通常のメモリスパイクはどう見分けますか?
スパイクはワークロード完了後に安定するか減少することが多いです。リークは通常、操作を繰り返すたびに持続的に増加し、アイドル時にもベースラインに戻りません。
メモリリークのデバッグでなぜファイルディスクリプターを確認するのですか?
リソースリークは同時に発生することが多いです。ソケットやファイルを閉じずに開き続けるプロセスは、ディスクリプターの増加とメモリ圧迫の両方を示すことがあります。
リークしているプロセスを見つけた後、どのツールを使うべきですか?
言語やプラットフォーム固有のプロファイラーを使いましょう:Swift/Objective-CにはXcode Memory Graph、Node.jsにはDevTools/clinic.js、Pythonにはmemory_profilerまたはtracemallocのワークフローが適しています。