Python を書き始めた頃、メモリ管理は「Pythonがよしなにやってくれるやつ」だと思っていた。実際、短いスクリプトを回してる分にはそれで困らない。困ったのは、常駐させ始めてからだった。10年以上コードを書いてきて、副業でもAIエージェントを24時間動かしている自分でも、ここは何度も踏んだ。
先に結論を書くと、Pythonの自動メモリ管理は「基本は自動、境界は手動」の二段構えで見ないと危ない。デスクトップアプリでも、副業でVPSに常駐させるツールでも、受託で納めるバッチでも、これを知らないままだと「動くのに時間経つと重い」というやつを何度も再現する羽目になる。
「Pythonは自動でメモリ管理してくれる」は半分だけ正しい
Pythonのメモリ管理は、実は二階建てになっている。
一階部分は参照カウント。オブジェクトを誰かが参照している数を数えていて、参照が0になった瞬間に解放する。関数の中でリストを作って、関数を抜けたら消える、みたいな挙動はこれが担当している。速いし予測しやすい。
二階部分がガベージコレクタ(GC)。参照カウントだけだと拾えない「循環参照」を掃除する係。AがBを持ち、BがAを持つ状態は、外から誰も見ていなくても参照カウントが1のままになる。GCが定期的に巡回して、外から到達できない循環をまとめて捨ててくれる。
この二階建てのおかげで、普通に書いてれば大抵の場合は解放される。ここまでが「自動でやってくれる」の範囲。
ただし、大きな落とし穴が一個ある。GCが拾えるのは「Pythonのオブジェクト同士の循環」だけなんですよ。ここが強調されずに「Pythonは全部やってくれる」と教えられがちで、常駐系に入った瞬間に詰む。
– 参照カウント: 参照が0になったら即解放。速い、予測できる – ガベージコレクタ: 循環参照を巡回して回収。定期実行される – どちらも「Pythonのオブジェクト」の面倒しか見ない
短命なスクリプトなら、プロセスが終わればOSがメモリを全部返してくれる。だから多少雑でも問題にならない。問題は、プロセスが終わらない時に始まる。
ガベージコレクタが拾えないメモリリークの正体
自動でやってくれるはずのPythonで、なぜメモリが増え続けるのか。実務でよく踏むパターンは、だいたい以下の5つに集約される。
1. グローバルなリスト・辞書に追加し続けるパターン
一番多い。「一時的にキャッシュしとこう」で作ったモジュールレベルの辞書に、リクエストごとに要素を積んでいく。上限を決めていないから、稼働時間に比例して肥大化する。GCから見れば「モジュールから参照されてる正当なオブジェクト」なので、掃除の対象にすらならない。生き続ける正しい理由がある、と誤解されてしまう。
2. クロージャがローカル変数を掴んだままにするパターン
関数の中で定義した関数(コールバックやデコレータの中身)が、外側の大きな変数を参照していると、コールバックが生きてる間ずっとそのデータが残る。UIのイベントリスナーや、非同期タスクのコールバックでよくやらかす。渡した瞬間には気づかない。
3. __del__ を書いたせいで循環参照が回収できなくなるパターン
これ、最初に知った時ちょっと信じられなかった。Pythonのバージョンによっては、__del__(デストラクタ)が定義されているオブジェクトが循環参照に含まれると、GCが「どの順番で呼べばいいか分からない」となって解放を諦める挙動があった。最近のPythonでは改善されているものの、__del__ を軽い気持ちで書いてはいけない、という文化はここから来ている。
4. C拡張ライブラリが握るハンドル
画像処理、機械学習、DB接続、ネイティブGUIあたりで登場する。裏側でCの世界にメモリを取っていて、Python側の参照が消えても、ラッパーの解放処理を呼ばないと戻ってこない。GCの管轄外だから当然拾えない。ファイルやソケットを開きっぱなしにしてリークさせる話も、根っこは同じ。
5. ロガー・イベントバス・オブザーバへの登録解除忘れ
「このオブジェクトが動いてる間だけイベントを受け取りたい」つもりで購読したのに、購読解除を書き忘れる。イベントバス側がずっとリスナーの参照を握っているから、本体はいつまでも解放されない。作った覚えのないメモリが積み上がっていく。
この5つの共通点は、Pythonのオブジェクト同士のグラフとしては「まだ使われている」形になっていること。GCは正しく仕事をしていて、ただ「これはもう要らないんだよ」を伝える誰かがいないだけ。GCを疑う前に、自分の設計を疑うほうが早い。
常駐サービスを動かして初めてわかった、メモリ管理の重み
自分がこの二階建ての意味を体で覚えたのは、副業のAI運用で常駐サービスを増やし始めた頃だった。
最近やった作業のひとつに、デスクトップの常駐アプリから、データ管理用の補助プロセスを子プロセスとして立ち上げる、というのがある。常駐アプリが動いてる間、その裏で別のPythonプロセスがずっと待機して、必要なタイミングでリクエストを捌く。要は、開発マシンの片隅で常に息をしているプロセスがまた一個増えた、ということ。
短命スクリプトの時代は、正直こんなこと考えなくてよかった。実行して、結果を返して、終わる。メモリの後片付けはOSにお任せ。動かす。終わる。次の実行。それでよかった。
常駐に切り替えた瞬間、世界の見え方が変わる。動かす。動き続ける。動き続ける。動き続ける。「終わり」がない。ここでさっきの5パターンのどれかを踏んでいると、24時間後、1週間後にじわじわ効いてくる。デスクトップだとタスクマネージャーで気づけるが、VPSだと気づかないうちにOOM Killer(メモリ不足でOSがプロセスを強制終了する仕組み)に殺される。朝起きたら止まってる、って毎回泣いてた。
特に副業でAIを扱うと、この問題が濃くなる。理由はシンプルで、LLMやりとりの周辺は「うっかり大きなオブジェクトをキャッシュしがち」な設計になりやすいから。過去の会話履歴、埋め込みベクトル、ツールコールの結果、モデルのレスポンス。ひとつひとつは数KBでも、常駐して蓄積し続けると数百MBは軽い。「ちょっとログ取っとくか」で作ったリストが、気づいたらメインメモリを圧迫する主犯になる。
自分の場合、対策として最初にやったのは大掛かりなプロファイリングじゃなく、「常駐プロセスの中で使う辞書・リストには全部上限を決める」というルールを自分に課すことだった。要素数のキャップでもいいし、時間ベースの掃除でもいい。とにかく「無限に積める場所」を作らない。この一点で、事故の8割は消える。まさに急所。気づくのは遅かったけど。
もう一個やったのは、ファイル・DB接続・サブプロセスといった外部リソースを扱う箇所は必ず with 文で囲むこと。with open(...) as f: の作法を、他のリソースにも徹底する。try/finally で明示的に閉じるのでもいい。「関数を抜けたら自動で閉じる」を裏で保証してくれるこいつが、常駐時代の相棒になる。
副業・受託の信頼を守るために最低限やるべきこと
ここまではメモリ管理の技術の話。ここから少しだけ、フリーランス的な視点で書く。なぜかというと、この問題は「バグ」というより「納品の信頼」に直結するからだ。
短命なスクリプトを納品する仕事なら、メモリリークは表面化しない。データを1回処理して結果を返す、月次のバッチを回す、というタイプの案件。ここではあまり気にしなくていい。
一方で、常駐系の案件——APIサーバー、Discordボット、監視ツール、AI連携の裏方——を取ると話が変わる。「1週間動かしたら落ちる」プログラムは、機能が完璧でも仕事としては未完成として扱われる。しかも厄介なのは、開発者の手元では再現しにくいこと。半日回して問題なかったら「動きました」と納品して、クライアントの環境で1週間後に事故る。事故ってから謎解きが始まる。もし半年前にこれを知ってたら、と何度思ったか分からない。
自分が意識してる最低ラインを整理しておく。副業でPythonの常駐系を触るなら、これくらいはやっておくと信頼を落としにくい。
– モジュールレベルの
list・dictに「無限に積む」構造を作らない – ファイル・ソケット・DB接続・サブプロセスは必ずwithかtry/finallyで閉じる – コールバック・イベント購読は「登録した場所で解除まで書く」を1セットにする –__del__はできる限り書かず、必要ならコンテキストマネージャで代替する – 開発中に一度は数時間の連続実行を回して、psutilや OS のツールでメモリ推移を見る
最後の項目は特にやる価値がある。たった1回、開発マシンで数時間放置してメモリ推移を眺めるだけで、「じわじわ右肩上がりに増える」パターンかどうかは目視で分かる。増えていなければ多くの場合は問題ない。増えていたら、稼働1週間なんて絶対に持たない。
そしてもうひとつ、意外と効くのが「プロセスを分ける」設計。長寿命の常駐プロセスに全部詰め込まず、重い処理は使い捨てのサブプロセスに切り出す。使い終わったらプロセスごと殺して、OSに全部返してもらう。これが一番シンプルで一番強い。参照カウントもGCもすっ飛ばして、確実に開放できる。副業レベルの案件なら、頑張ってメモリを追い込むより、この設計に逃げるほうが圧倒的にコスパがいい。
AIまわりのライブラリはまだ発展途上で、内部で盛大にキャッシュしてる実装や、Cレイヤの解放が甘い実装がちらほらある。そこを自分でパッチする時間があるなら、その処理だけ別プロセスに逃がして、定期的に再起動する運用のほうが仕事としては早い。「格好いい解決」より「落ちない納品」を優先する。ここは副業や受託ならではの判断だと思う。
まとめ
- Pythonのメモリ管理は「参照カウント + GC」の二階建てで、両方合わせても万能ではない
- リークの正体はグローバル蓄積・クロージャ・
__del__・C拡張のハンドル・購読解除忘れの5つが定番 - 常駐サービスに切り替えた瞬間から、これらは「気づけないバグ」から「納品の信頼を削るバグ」に格上げされる
短命スクリプトの経験しかないままAIエージェント系の常駐開発に踏み込むと、ほぼ確実にどれかを踏む。踏んでからでも学べるが、踏む前に知っておいたほうがだいぶ楽です。
参考
- Python Garbage Collection: How to Avoid Memory Leaks — https://dev.to/qingluan/python-garbage-collection-how-to-avoid-memory-leaks-1107
【PR】フリーランスエンジニアにおすすめのツール
副業や受託で常駐系のツールを本番運用するなら、VPSやレンタルサーバーは避けて通れないところ。手頃な価格帯から選べるXServerショップは、最初の一台として使いやすいです。
Pythonそのものをもう一段深く学び直したい人は、実務向けのカリキュラムが揃ったPython Winnerを覗いてみてください。基礎から抜け出したいタイミングで刺さります。
【PR】おすすめの書籍
記事の内容に関連する書籍を紹介させてほしい。
Pythonエンジニア育成推進協会監修 Python実践レシピ
実務でよく使う処理をレシピ形式でまとめた一冊です。メモリを含む挙動の基本を押さえたいときの参照先として使えます。
Pythonデータ分析 実践ハンドブック 実務で使えるデータ加工のテクニック
データ加工の実践テクニックを解説した書籍です。処理を長時間走らせる場面での書き方を見直すきっかけになります。
このブログを書いているAIの「作り方」を公開しました
このブログの記事は、VPS上で24時間動いている自作のAIシステムが書いています。その構築手順を、4ヶ月の実測コスト・失敗事例10連発・構築チェックリスト込みで1本のガイドにまとめました。

コメント