- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
メモリを回収する仕組みと、ファイルや接続を閉じる仕組みを分けて見ます。
まず結論
GC が約束しているのは「まだ届くものを回収しない」ことまでで、いつ回収するか、そもそも回収するかは約束していません。Python と JavaScript の公式文書はそう書いており、Go の文書はファイナライザについて同じことを書いています。
だからファイルや接続は GC に任せず、with・defer・スコープの終わりのように、プログラムの流れで閉じる時刻を決めます。GC の無い Rust でも、参照の循環があれば後始末は走りません。
Go 1.27.1 で、runtime.SetFinalizer を付けたオブジェクトへの参照を nil にして捨て、そのまま main を終えます。finalizer は何回走ったでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
GC が約束しているのは「まだ届くものを消さない」ことだけ
GC(ガベージコレクション)は、プログラムから届かなくなったメモリを回収します。では、ファイルを開いたオブジェクトが届かなくなったら、ファイルも閉じられるのでしょうか。
Python の Data model は、まず回収そのものについてこう書いています。
Objects are never explicitly destroyed; however, when they become unreachable they may be garbage-collected. An implementation is allowed to postpone garbage collection or omit it altogether — it is a matter of implementation quality how garbage collection is implemented, as long as no objects are collected that are still reachable.
言語として約束しているのは最後の一文、まだ届くものは回収しないことだけです。延期してもよいし、回収しなくてもよい。そのうえで、ファイルのような外部の資源について続けます。
It is understood that these resources are freed when the object is garbage-collected, but since garbage collection is not guaranteed to happen, such objects also provide an explicit way to release the external resource, usually a close() method. Programs are strongly recommended to explicitly close such objects.
JavaScript も同じ立場です。ECMA-262 §9.9.1 は This specification does not make any guarantees that any object or symbol will be garbage collected と書いています。Go の SetFinalizer の文書も There is no guarantee that finalizers will run before a program exits です。
- ・GC の保証は「まだ届くものを回収しない」ことだけ
- ・ファイルや接続を閉じる時刻は、上の2段ではなく最下段で決める
3つの言語で、捨てたあとに何が起きたか
同じことを Python 3.14.7・Node v24.21.0・Go 1.27.1 で試しました(いずれも Docker、aarch64)。Python は、循環の無いオブジェクト a と、互いを指し合う b と c を作って捨て、最後に gc.collect() を呼びます。
import gc
class R:
def __init__(self, n): self.n = n
def __del__(self): print(" del", self.n)
print("1: no cycle"); a = R("a"); a = None; print(" after a=None")
print("2: cycle"); b = R("b"); c = R("c"); b.o = c; c.o = b; b = c = None; print(" after b=c=None")
print(" collect ->", gc.collect())
print("3: end")
Node は無名のオブジェクトを FinalizationRegistry に登録してすぐ手放し、Go は SetFinalizer を付けた値を nil で捨ててから main を終えます。
### run 1 python:3.14-slim1: no cycledel aafter a=None2: cycleafter b=c=Nonedel bdel ccollect -> 23: endexit=0### run 1 node:24.21.0-alpinescript endsexit=0### run 1 golang:1.27-alpinemain returnsexit=0
Python では、循環の無い a は手放した直後に del a が出ました。循環する b と c は after b=c=None の時点では何も起きず、gc.collect() を呼んで初めて __del__ が呼ばれ、2 が返りました(回収できたものと、できなかったものの合計数)。
即座に消えたのは、CPython が参照カウントを使っているからです。ただし Data model はこれを CPython implementation detail の枠に置き、同じ枠で so you should always close files explicitly と書いています。言語の約束ではありません。
Node と Go では、後始末の関数は一度も呼ばれないままプロセスが終わりました。
Python 3.14.7 で、循環の無いオブジェクトは `a = None` の直後に `__del__` が呼ばれました。この挙動の位置づけとして正しいのはどれですか。
閉じる時刻は、GC とは別の仕組みで決める
GC のある言語では後始末の時刻を決められない、という理解も誤りです。どの言語も、制御の流れで時刻を決める仕組みを GC と別に持っています。
- Python は
with文とtry…finally。Data model 自身がThe try…finally statement and the with statement provide convenient ways to do thisと勧めています。 - Go は
defer。Effective Go は、関数が戻る直前に呼ぶこの仕組みの代表例として、ファイルを閉じることを挙げています。 - Rust は、変数がスコープを抜けた時点で drop を呼びます。
ファイナライザは、閉じ忘れたときの安全網にとどまります。Go の文書は、長く動くプログラムでメモリ以外の資源を解放するなら役に立つとしたうえで、it would be a mistake to depend on a finalizer to flush an in-memory I/O buffer such as a bufio.Writer, because the buffer would not be flushed at program exit と明言しています。
新しいコード向けの AddCleanup も、説明は Some time after ptr is no longer reachable で、呼ぶ時刻を決めていません。
GC の無い Rust でも、後始末が走らないことがある
Rust には GC がありません。値はスコープの終わりで drop されます。では Rust なら後始末は必ず走るのでしょうか。rustc 1.98.1 で、スコープで囲んだ値と、自分自身を指す Rc を比べました。
struct R(&'static str);
impl Drop for R { fn drop(&mut self) { println!(" drop {}", self.0); } }
fn main() {
{ let _a = R("a"); println!(" in scope"); }
println!(" after scope");
use std::rc::Rc; use std::cell::RefCell;
struct N { _o: RefCell<Option<Rc<N>>>, _r: R }
let x = Rc::new(N { _o: RefCell::new(None), _r: R("cycle") });
*x._o.borrow_mut() = Some(x.clone());
drop(x);
println!(" end of main");
}
### run 1 rust:1-slimin scopedrop aafter scopeend of mainexit=0
スコープを抜けた瞬間に drop a が出ています。一方、drop(x) で手放した循環のほうは、end of main の後も含め、プロセスが終わるまで drop cycle が一度も出ませんでした。
これは Rust の約束の範囲内です。std::mem::forget の文書は Rust’s safety guarantees do not include a guarantee that destructors will always run と書き、例として Rc の循環と process::exit を挙げています。Rust Book も memory leaks are memory safe in Rust です。
「Rust は所有権があるので、値の後始末(drop)は必ず走る」という説明は妥当ですか。
Python の循環参照(GC がある)
b と c が互いを指しているので、参照の数が0にならず、手放しても __del__ は呼ばれませんでした。gc.collect() を呼んだ時点で初めて回収されました。
2: cycleafter b=c=Nonedel bdel ccollect -> 2
Rust の Rc 循環(GC が無い)
自分を指す Rc は参照の数が0にならず、drop(x) の後も drop は呼ばれませんでした。拾い上げる GC も無く、drop cycle はプロセスが終わるまで一度も出ませんでした。
after scopeend of mainexit=0
GC がある言語と無い言語で、循環を作ったときの後始末はどう違い、どこが同じでしょうか。
後始末が走るきっかけを、それぞれ何が作っているかを比べてください。
この章のまとめ
- GC が約束するのは まだ届くものを回収しないこと だけ。Python・ECMA-262・Go の文書は、回収やファイナライザの実行を保証していない。
- CPython の即時回収は 実装の詳細。循環があると
gc.collect()まで__del__は呼ばれなかった。 - Node v24.21.0 と Go 1.27.1 の実演では、後始末の関数は 一度も呼ばれずに 終了した。
- 閉じる時刻は with・defer・スコープの終わり で決められる。ファイナライザは安全網にとどまる。
- GC の無い Rust でも、Rc の循環や mem::forget では drop が走らない。
- 手元の Python 3.14.7 は世代別 GC。インクリメンタル GC は 3.14.0〜3.14.4 だけ。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
ECMA-262(2026年9月の draft)は、到達できなくなったオブジェクトの回収について何と書いていますか。
用語集(3語)
- 到達可能性
- プログラムが変数などをたどって、そのオブジェクトに届くかどうか。GC はこれが無くなったものを回収してよいが、いつ回収するかは約束しない。
- ファイナライザ
- 回収の前後に呼ばれる後始末の関数。Python の __del__、Go の SetFinalizer、JavaScript の FinalizationRegistry の cleanup など。呼ばれる時刻も、呼ばれるかどうかも保証されない。
- 参照の循環
- オブジェクトが互いを(または自分を)指し合う状態。参照の数が0にならないので、参照カウントだけでは回収できない。
出典(一次ソース)
- Python 3.14 — Data model §3.1 Objects, values and types / object.__del__ ↗
- Python 3.14 — gc module ↗
- What's New in Python 3.14 — Garbage collection ↗
- Go — package runtime (SetFinalizer, AddCleanup) ↗
- Effective Go — Defer ↗
- A Guide to the Go Garbage Collector ↗
- ECMA-262 (Draft, September 20, 2026) §9.9.1 ↗
- MDN — FinalizationRegistry ↗
- The Rust Programming Language — Reference Cycles Can Leak Memory ↗
- Rust std — std::mem::forget ↗