[インデックス 16685] ファイルの概要
このコミットは、Goランタイムにおけるdefer
ステートメントに関連するメモリリークの修正を目的としています。特に、defer
された関数がクロージャやスタックセグメントへのポインタを保持し続けることによって発生するメモリリークに対処しています。
コミット
commit fd23958f49f0967c9a5999ffc2e33740f246a11a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jul 1 17:36:08 2013 -0400
runtime: fix memory leaks due to defers
fn can clearly hold a closure in memory.
argp/pc point into stack and so can hold
in memory a block that was previously
a large stack serment.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/10784043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fd23958f49f0967c9a5999ffc2e33740f246a11a
元コミット内容
Goランタイムにおいて、defer
ステートメントが原因で発生するメモリリークを修正します。具体的には、defer
された関数(fn
)がクロージャをメモリ上に保持し続ける可能性があり、また、argp
(引数ポインタ)やpc
(プログラムカウンタ)がスタックを指し示すことで、以前に大きなスタックセグメントであったメモリブロックを保持し続ける可能性がありました。このコミットは、これらのポインタが不要になったメモリを適切に解放できるようにすることで、メモリリークを防ぎます。
変更の背景
Goのdefer
ステートメントは、関数の実行が終了する直前に指定された関数を呼び出すメカニズムを提供します。これはリソースの解放(ファイルクローズ、ロック解除など)に非常に便利です。しかし、defer
された関数がクロージャである場合、そのクロージャは外部スコープの変数をキャプチャすることがあります。また、defer
の実行コンテキスト(引数やプログラムカウンタなど)は、Defer
構造体内に保存されます。
このコミット以前のGoランタイムでは、defer
が実行され、そのDefer
構造体が不要になった際に、構造体内のポインタフィールドが適切にゼロクリアされていませんでした。これにより、ガベージコレクタ(GC)がこれらのポインタを有効な参照と誤認し、実際には不要になったメモリブロック(特に、クロージャが参照していたオブジェクトや、以前の大きなスタックセグメント)を解放できないという問題が発生していました。これはメモリリークとして現れ、特に長時間稼働するアプリケーションや、多数のdefer
が使用されるアプリケーションで顕著になる可能性がありました。
この問題は、ガベージコレクタが参照を追跡する際に、古いポインタが残っていると、そのポインタが指すメモリ領域がまだ使用中であると判断してしまう「保守的なGC」の側面と関連しています。GoのGCは進化していますが、このようなケースでは明示的なポインタの無効化が必要でした。
前提知識の解説
Goのdefer
ステートメント
defer
は、Go言語の制御構造の一つで、現在の関数がリターンする直前(パニックが発生した場合も含む)に、指定された関数呼び出しを遅延実行させるものです。複数のdefer
がある場合、それらはLIFO(後入れ先出し)の順序で実行されます。
func example() {
file := openFile("data.txt")
defer closeFile(file) // 関数終了時にcloseFileが呼ばれる
// ファイル操作
}
クロージャ (Closures)
クロージャは、関数が定義された環境(レキシカルスコープ)を記憶し、その環境内の変数にアクセスできる関数です。defer
される関数がクロージャである場合、そのクロージャは外部の変数をキャプチャし、その変数が指すメモリを保持し続ける可能性があります。
func createDeferFunc(value int) func() {
return func() {
fmt.Println("Deferred value:", value) // valueをキャプチャ
}
}
func main() {
i := 10
defer createDeferFunc(i)() // iの値をキャプチャしたクロージャがdeferされる
i = 20 // deferされる関数は10を出力する
}
ガベージコレクション (Garbage Collection, GC)
Goランタイムには自動メモリ管理のためのガベージコレクタが組み込まれています。GCは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能(参照されていない)になったメモリ領域を自動的に解放し、再利用可能にします。GCは、プログラムの実行中に定期的に、または特定の条件(メモリ使用量など)に基づいてトリガーされます。GCが正しく機能するためには、有効なポインタと無効なポインタを正確に区別できる必要があります。
Defer
構造体 (内部実装)
Goランタイムの内部では、defer
ステートメントはruntime.Defer
のような構造体として表現されます。この構造体は、defer
される関数の情報(関数ポインタ、引数、レジスタの状態など)を保持します。これらの情報は、関数が実際に実行されるまでメモリ上に保持されます。
メモリリーク
メモリリークは、プログラムが動的に割り当てたメモリを使い終わった後も解放せず、そのメモリがガベージコレクタによって回収されない状態を指します。これにより、利用可能なメモリが徐々に減少し、最終的にはプログラムのパフォーマンス低下やクラッシュにつながる可能性があります。
技術的詳細
このコミットの核心は、runtime/panic.c
内のfreedefer
関数におけるメモリクリアの範囲の変更です。
freedefer
関数は、defer
された関数が実行され、そのDefer
構造体が不要になった際に呼び出されます。この関数の目的は、Defer
構造体に関連するメモリを解放し、特に構造体内に残っている可能性のあるポインタを無効化することです。これにより、ガベージコレクタがこれらのポインタを追跡して、既に不要なメモリを保持し続けることを防ぎます。
変更前は、Defer
構造体のargs
フィールドが指す領域のみがクリアされていました。しかし、Defer
構造体自体には、fn
(関数ポインタ)、argp
(引数ポインタ)、pc
(プログラムカウンタ)といった、メモリ上の他の場所を指す可能性のあるポインタが含まれています。特に、fn
がクロージャを指す場合、そのクロージャはヒープ上のオブジェクトをキャプチャしている可能性があります。また、argp
やpc
はスタック上の領域を指すことがあり、以前に大きなスタックセグメントが割り当てられていた場合、その領域への参照が残ってしまうと、GCがそのスタックセグメントを解放できない原因となります。
このコミットでは、Defer
構造体全体(ただし、special
フラグが立っている場合は除く)を、その先頭から、args
フィールドの終わりまで、つまりDefer
構造体自体とそれに続く引数領域全体をゼロクリアするように変更しました。これにより、fn
, argp
, pc
といったポインタフィールドも確実にゼロクリアされ、ガベージコレクタがこれらのポインタを有効な参照として誤認することがなくなります。
新しいテストケースtest/deferfin.go
は、この修正が正しく機能することを検証するために追加されました。このテストは、defer
されたクロージャがヒープ上のオブジェクト(この場合はnew(int)
で作成されたv
)をキャプチャし、そのオブジェクトにruntime.SetFinalizer
を設定します。finalizer
は、オブジェクトがGCによって回収される直前に実行される関数です。テストは、多数のこのようなdefer
とオブジェクトを作成し、GCを複数回実行した後、すべてのfinalizer
が呼び出されたことを確認します。もしメモリリークがあれば、finalizer
が呼び出されず、テストは失敗します。このテストは、32ビット環境では部分的に保守的なGCの挙動により動作しない可能性があるため、amd64
アーキテクチャに限定されています。
コアとなるコードの変更箇所
src/pkg/runtime/panic.c
--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -104,11 +104,15 @@ popdefer(void)
static void
freedefer(Defer *d)
{
+ int32 total;
+
if(d->special) {
if(d->free)
runtime·free(d);
} else {
- runtime·memclr((byte*)d->args, d->siz);
+ // Wipe out any possible pointers in argp/pc/fn/args.
+ total = sizeof(*d) + ROUND(d->siz, sizeof(uintptr)) - sizeof(d->args);
+ runtime·memclr((byte*)d, total);
}
}
test/deferfin.go
(新規追加ファイル)
// run
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Test that defers do not prevent garbage collection.
package main
import (
"runtime"
"sync"
"sync/atomic"
"time"
)
var sink func()
func main() {
// Does not work on 32-bits due to partially conservative GC.
// Try to enable when we have fully precise GC.
if runtime.GOARCH != "amd64" {
return
}
N := 10
count := int32(N)
var wg sync.WaitGroup
wg.Add(N)
for i := 0; i < N; i++ {
go func() {
defer wg.Done()
v := new(int)
f := func() {
if *v != 0 {
panic("oops")
}
}
if *v != 0 {
// let the compiler think f escapes
sink = f
}
runtime.SetFinalizer(v, func(p *int) {
atomic.AddInt32(&count, -1)
})
defer f()
}()
}
wg.Wait()
for i := 0; i < 3; i++ {
time.Sleep(10 * time.Millisecond)
runtime.GC()
}
if count != 0 {
println(count, "out of", N, "finalizer are not called")
panic("not all finalizers are called")
}
}
コアとなるコードの解説
src/pkg/runtime/panic.c
の変更点
freedefer
関数内の変更は以下の通りです。
- 変更前:
runtime·memclr((byte*)d->args, d->siz);
- これは、
Defer
構造体内のargs
フィールドが指すメモリ領域(defer
された関数の引数が格納されている場所)のみをゼロクリアしていました。d->args
はDefer
構造体の末尾に続く可変長データの一部を指します。
- これは、
- 変更後:
total = sizeof(*d) + ROUND(d->siz, sizeof(uintptr)) - sizeof(d->args); runtime·memclr((byte*)d, total);
total
変数が導入され、クリアすべきメモリの総バイト数を計算しています。sizeof(*d)
:Defer
構造体自体のサイズ。これにはfn
,argp
,pc
などのポインタフィールドが含まれます。ROUND(d->siz, sizeof(uintptr))
:defer
された関数の引数のサイズをuintptr
の倍数に切り上げたもの。これはアライメントを考慮したサイズです。- sizeof(d->args)
:Defer
構造体内のargs
フィールドは、実際には可変長引数領域の開始点を示すためのプレースホルダであり、そのサイズはDefer
構造体自体のサイズ計算には含めるべきではありません。この減算により、Defer
構造体の固定部分と、それに続く引数領域全体を正確にカバーするサイズが計算されます。
runtime·memclr((byte*)d, total);
Defer
構造体の先頭((byte*)d
)からtotal
バイト分をゼロクリアします。これにより、Defer
構造体内のすべてのポインタフィールド(fn
,argp
,pc
など)と、それに続く引数領域が確実にゼロクリアされます。d->special
がtrue
の場合は、defer
が特別な種類(例えば、go
キーワードで開始されたゴルーチンに関連するdefer
など)であり、異なるメモリ管理ロジックが適用されるため、このゼロクリアは行われません。
この変更により、Defer
構造体が解放される際に、その中に残っていた古いポインタがガベージコレクタによって追跡されることがなくなり、関連するメモリリークが解消されます。
test/deferfin.go
の解説
このテストは、defer
がガベージコレクションを妨げないことを検証するためのものです。
- アーキテクチャチェック:
runtime.GOARCH != "amd64"
の場合、テストはスキップされます。これは、32ビット環境ではGoのGCが部分的に保守的であり、このテストの意図する挙動(正確なGCによるメモリ解放)を保証できないためです。 - N個のゴルーチン生成:
N
個(ここでは10個)のゴルーチンを起動します。 v
とf
の作成: 各ゴルーチン内で、v := new(int)
によってヒープ上に新しい整数ポインタv
を作成します。そして、f := func() { ... }
というクロージャを定義します。このクロージャf
は、外部スコープの変数v
をキャプチャしています。runtime.SetFinalizer
の設定:runtime.SetFinalizer(v, func(p *int) { atomic.AddInt32(&count, -1) })
を呼び出し、v
がGCによって回収される直前にcount
をデクリメントするファイナライザを設定します。count
は、GCによって回収されたv
の数を追跡します。defer f()
: 定義したクロージャf
をdefer
します。これにより、ゴルーチンが終了する際にf
が実行されます。wg.Wait()
: すべてのゴルーチンが終了するのを待ちます。- GCの実行:
runtime.GC()
を複数回(3回)呼び出し、ガベージコレクションを強制的に実行します。time.Sleep
は、GCが完了するのに十分な時間を与えます。 - 結果の検証: 最終的に
count
が0であることを確認します。もしcount
が0でなければ、それは一部のv
オブジェクトがGCによって回収されず、そのファイナライザが呼び出されなかったことを意味します。これは、defer
されたクロージャがv
への参照を保持し続け、メモリリークが発生していることを示します。
このテストは、defer
されたクロージャがキャプチャしたオブジェクトが、defer
の実行後も不必要にメモリに保持されないことを保証するための重要な検証手段です。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメント: https://go.dev/tour/flowcontrol/12 - Goのガベージコレクションに関する一般的な情報: https://go.dev/doc/gc-guide
参考にした情報源リンク
- Goのソースコード(
src/pkg/runtime/panic.c
) - Goのソースコード(
test/deferfin.go
) - Goのコミット履歴 (GitHub)
- Go言語の
defer
、クロージャ、ガベージコレクションに関する一般的な知識。 - Goの
Defer
構造体に関する内部実装の議論(GoのメーリングリストやIssueトラッカーなど、当時の情報源を想定)