Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 11 / 12·約 8 分·第25期 どの言語にもある仕組み

回収されることと、閉じられることは別

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. 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 が回収する実装が時刻を選ぶ
ファイナライザが走る走る保証が無い
時刻をプログラムが決める道筋
close() を呼ぶwith・defer・スコープの終わり
  • ・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。run 2 も同一)コマンド出力
### run 1 python:3.14-slim
1: no cycle
del a
after a=None
2: cycle
after b=c=None
del b
del c
collect -> 2
3: end
exit=0
### run 1 node:24.21.0-alpine
script ends
exit=0
### run 1 golang:1.27-alpine
main returns
exit=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。run 2 も同一)コマンド出力
### run 1 rust:1-slim
in scope
drop a
after scope
end of main
exit=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)は必ず走る」という説明は妥当ですか。

⇄ 2つを見比べる
具体例 A

Python の循環参照(GC がある)

b と c が互いを指しているので、参照の数が0にならず、手放しても __del__ は呼ばれませんでした。gc.collect() を呼んだ時点で初めて回収されました。

Python 3.14.7(上の出力の抜粋)コマンド出力
2: cycle
after b=c=None
del b
del c
collect -> 2
具体例 B

Rust の Rc 循環(GC が無い)

自分を指す Rc は参照の数が0にならず、drop(x) の後も drop は呼ばれませんでした。拾い上げる GC も無く、drop cycle はプロセスが終わるまで一度も出ませんでした。

rustc 1.98.1(上の出力の抜粋)コマンド出力
after scope
end of main
exit=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 だけ。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

ECMA-262(2026年9月の draft)は、到達できなくなったオブジェクトの回収について何と書いていますか。

用語集(3語)
到達可能性
プログラムが変数などをたどって、そのオブジェクトに届くかどうか。GC はこれが無くなったものを回収してよいが、いつ回収するかは約束しない。
ファイナライザ
回収の前後に呼ばれる後始末の関数。Python の __del__、Go の SetFinalizer、JavaScript の FinalizationRegistry の cleanup など。呼ばれる時刻も、呼ばれるかどうかも保証されない。
参照の循環
オブジェクトが互いを(または自分を)指し合う状態。参照の数が0にならないので、参照カウントだけでは回収できない。