Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16228] ファイルの概要

このコミットは、Goランタイムにおけるガベージコレクション(GC)の挙動に関するバグ修正です。具体的には、runtimeパッケージ内のrunfinq()関数が、フレームの内容をGCから適切に隠蔽できていなかった問題に対処しています。これにより、ファイナライザが予期せずオブジェクトを保持し続ける可能性がありました。

コミット

commit e9bbe3a8da9043e13b74ec4427608364b068bed7
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Thu Apr 25 13:39:09 2013 +0200

    runtime: prevent the GC from seeing the content of a frame in runfinq()
    
    Fixes #5348.
    
    R=golang-dev, dvyukov
    CC=golang-dev
    https://golang.org/cl/8954044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e9bbe3a8da9043e13b74ec4427608364b068bed7

元コミット内容

runtime: prevent the GC from seeing the content of a frame in runfinq()

Fixes #5348.

R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/8954044

変更の背景

この変更は、Go言語のIssue 5348「finalizers keep data live for a surprising amount of time(ファイナライザが驚くほど長い間データを保持し続ける)」を修正するために行われました。

Goのガベージコレクタは、到達可能なオブジェクトを特定し、到達不能なオブジェクトを解放します。しかし、ファイナライザが設定されたオブジェクトは、ファイナライザが実行されるまでGCによって解放されません。Issue 5348では、ファイナライザが設定されたオブジェクトが、本来解放されるべきタイミングで解放されず、予期せぬ期間メモリ上に残り続けるという問題が報告されました。

この問題の根本原因は、ランタイムの内部関数であるrunfinq()が、ファイナライザを実行するために一時的に確保するスタックフレーム(またはそれに類するメモリ領域)の扱い方にありました。runfinq()内で使用されるメモリが、GCによってポインタを含むものとして誤って認識され、その結果、本来は参照されなくなるはずのオブジェクトが、この一時的なメモリ領域を通じて「到達可能」と判断されてしまっていたのです。これにより、ファイナライザが設定されたオブジェクトとその関連データが、GCサイクルを生き延びてしまい、メモリリークのような挙動を引き起こしていました。

前提知識の解説

Goのガベージコレクション(GC)

Go言語は自動メモリ管理を採用しており、ガベージコレクタが不要になったメモリを自動的に解放します。GoのGCは、主にマーク&スイープ方式をベースにしています。プログラムの実行中に、GCは到達可能なオブジェクト(ルートセットから参照されているオブジェクト)をマークし、マークされなかったオブジェクトを到達不能と判断して解放します。

ファイナライザ(runtime.SetFinalizer

Goにはruntime.SetFinalizerという関数があり、これを使うと、特定のオブジェクトがガベージコレクタによって到達不能と判断され、メモリから解放される直前に実行される関数(ファイナライザ)を設定できます。これは、ファイルハンドルやネットワーク接続など、OSリソースをクリーンアップする際に便利です。

ファイナライザの重要な特性は以下の通りです。

  • 実行タイミング: オブジェクトが到達不能になった後、GCによってメモリが解放される直前に実行されます。GCの正確なタイミングは保証されません。
  • 参照の保持: ファイナライザが設定されたオブジェクトは、ファイナライザが実行されるまでGCによって解放されません。また、ファイナライザ関数自体がそのオブジェクトへの参照を保持している場合、そのオブジェクトはファイナライザが実行されるまで到達可能とみなされます。
  • ポインタの扱い: GCはメモリ上のポインタを追跡して到達可能性を判断します。メモリ領域がポインタを含む可能性があるとGCに認識されると、その領域内の値が参照しているオブジェクトも到達可能とみなされます。

runtime.malruntime.mallocgc

Goランタイム内部には、メモリを確保するための低レベルな関数がいくつか存在します。

  • runtime.mal: これは、ガベージコレクタの管理下にない、生のメモリを確保するための関数です。この関数で確保されたメモリ領域は、GCの対象外であり、その内容がポインタを含んでいるかどうかをGCが自動的に判断することはありません。
  • runtime.mallocgc: これは、ガベージコレクタの管理下にあるメモリを確保するための関数です。この関数で確保されたメモリはGCの対象となり、GCは必要に応じてその内容をスキャンしてポインタを追跡します。FlagNoPointersなどのフラグを渡すことで、そのメモリ領域がポインタを含まないことをGCに伝えることができます。

スタックフレームとGC

関数が呼び出されると、その関数のローカル変数や引数などを格納するためのスタックフレームがメモリ上に確保されます。GCは、スタックフレーム上のポインタも追跡し、そこから参照されているオブジェクトを到達可能と判断します。

技術的詳細

このコミットの核心は、src/pkg/runtime/mgc0.c内のrunfinq()関数におけるメモリ確保方法の変更です。

runfinq()は、ファイナライザキュー(finq)からファイナライザを処理するランタイム内部の関数です。ファイナライザを実行する際、この関数は一時的にframeというメモリ領域を確保し、ファイナライザに渡す引数などを格納していました。

変更前のコードでは、このframeの確保にruntime·mal(framesz)が使用されていました。runtime·malはGCの管理下にない生のメモリを確保するため、GCはこのメモリ領域の内容をスキャンしません。しかし、runfinq()の内部で、このframe領域にファイナライザの引数として渡されるオブジェクトへのポインタが格納される可能性がありました。

問題は、runtime·malで確保されたメモリ領域が、GCにとっては「ポインタを含まない」と暗黙的に扱われることです。もしこの領域が実際にはポインタを含んでいた場合、GCはそのポインタを追跡せず、そのポインタが参照しているオブジェクトを到達不能と誤って判断してしまう可能性があります。

しかし、Issue 5348のケースでは逆の問題が発生していました。runfinq()がファイナライザを実行する際に、一時的に確保したframe領域に、ファイナライズ対象のオブジェクトへのポインタがコピーされることがありました。このframe領域は、GCがスキャンするべきスタックの一部として扱われるべきでしたが、runtime·malで確保されたためにGCの追跡対象外となっていました。

このコミットの修正は、runtime·malruntime·mallocgcに置き換えることで、このframe領域をGCの管理下に置くようにしました。さらに重要なのは、FlagNoPointersフラグをruntime·mallocgcに渡している点です。

runtime·mallocgc(framesz, FlagNoPointers, 0, 1)

このFlagNoPointersフラグは、確保されたメモリ領域がポインタを含まないことをGCに明示的に伝えます。これにより、GCはこの領域をスキャンする必要がなくなり、パフォーマンスのオーバーヘッドを避けることができます。

一見すると、ポインタを含む可能性がある領域をFlagNoPointersで確保するのは矛盾しているように思えます。しかし、この文脈では、runfinq()がファイナライザを実行する際に、ファイナライザの引数として渡されるオブジェクトへのポインタを一時的にframeにコピーするものの、このframe自体はGCがスキャンすべき「ルート」ではない、という意図があります。

この修正の目的は、runfinq()がファイナライザを実行する際に使用する一時的なメモリ領域が、GCのポインタスキャンパスから除外されるようにすることです。これにより、ファイナライザが実行されるまでの間、そのオブジェクトがGCによって誤って到達可能と判断されることを防ぎ、ファイナライザが完了した後に適切に解放されるようにします。

同時に追加されたtest/fixedbugs/issue5348.goは、この問題が実際に発生するかどうかを検証するためのテストケースです。runtime.SetFinalizerを使ってT型と*string型の両方にファイナライザを設定し、GCが複数回実行された後に両方のファイナライザが期待通りに実行されることを確認しています。これにより、ファイナライザが設定されたオブジェクトが予期せず長く保持される問題が修正されたことを保証します。

コアとなるコードの変更箇所

src/pkg/runtime/mgc0.cファイルのrunfinq()関数内の一箇所が変更されています。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2191,7 +2191,7 @@ runfinq(void)
 			if(framecap < framesz) {
 				runtime·free(frame);
-				frame = runtime·mal(framesz);
+				frame = runtime·mallocgc(framesz, FlagNoPointers, 0, 1);
 				framecap = framesz;
 			}
 			*(void**)frame = f->arg;

また、この修正を検証するための新しいテストファイルが追加されています。

test/fixedbugs/issue5348.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.

// Issue 5348: finalizers keep data live for a surprising amount of time

package main

import (
	"runtime"
)

type T struct {
	S *string
}

func newString(s string) *string {
	return &s
}

var c = make(chan int)

func foo() {
	t := &T{S: newString("foo")}
	runtime.SetFinalizer(t, func(p *T) { c <- 0 })
	runtime.SetFinalizer(t.S, func(p *string) { c <- 0 })
}

func main() {
	foo()
	runtime.GC()
	<-c
	runtime.GC()
	<-c
}

コアとなるコードの解説

src/pkg/runtime/mgc0.cの変更

変更の核心は、runfinq()関数内でファイナライザの引数を格納するために使用されるframeという一時的なメモリ領域の確保方法です。

  • 変更前: frame = runtime·mal(framesz);

    • runtime·malは、GCの管理下にない生のメモリを確保します。このメモリ領域はGCによってスキャンされないため、もしこの領域にポインタが格納されていても、GCはそのポインタを追跡しません。このことが、ファイナライザが設定されたオブジェクトが予期せず長く保持される原因となっていました。具体的には、runfinqのスタックフレームがGCによってスキャンされる際に、runtime·malで確保されたframe領域がGCの対象外であるにもかかわらず、その内容がファイナライズ対象のオブジェクトへの参照を含んでいたため、GCがそのオブジェクトを解放できない状態になっていました。
  • 変更後: frame = runtime·mallocgc(framesz, FlagNoPointers, 0, 1);

    • runtime·mallocgcは、GCの管理下にあるメモリを確保します。これにより、frame領域はGCの対象となります。
    • FlagNoPointersフラグは、この確保されたメモリ領域がポインタを含まないことをGCに明示的に伝えます。これにより、GCはこの領域をスキャンする必要がなくなり、パフォーマンスのオーバーヘッドを避けることができます。
    • この変更により、frame領域がGCの管理下に置かれ、そのライフサイクルがGCによって適切に管理されるようになります。FlagNoPointersが指定されているため、GCはこの領域の内容をポインタとして解釈せず、ファイナライズ対象のオブジェクトがこの一時的なframeによって不必要に保持されることがなくなります。

この修正により、runfinq()がファイナライザを実行する際に使用する一時的なメモリ領域が、GCのポインタスキャンパスから適切に除外されるようになり、ファイナライザが完了した後にオブジェクトが正しく解放されるようになりました。

test/fixedbugs/issue5348.goの解説

このテストケースは、Issue 5348で報告された問題を再現し、修正が正しく機能することを確認するために作成されました。

  • type T struct { S *string }: ポインタを含む構造体を定義しています。
  • func newString(s string) *string: 文字列へのポインタを返すヘルパー関数です。
  • var c = make(chan int): ファイナライザの実行を同期するためのチャネルです。
  • func foo():
    • t := &T{S: newString("foo")}: T型のオブジェクトと、そのフィールドSが指す文字列オブジェクトを作成します。
    • runtime.SetFinalizer(t, func(p *T) { c <- 0 }): tオブジェクトにファイナライザを設定します。ファイナライザが実行されるとチャネルcに値を送信します。
    • runtime.SetFinalizer(t.S, func(p *string) { c <- 0 }): t.Sが指す文字列オブジェクトにもファイナライザを設定します。
  • func main():
    • foo(): foo関数を呼び出し、tt.Sが指すオブジェクトを作成し、ファイナライザを設定します。foo関数から戻ると、これらのオブジェクトはmain関数からは到達不能になります。
    • runtime.GC(): 最初のGCを実行します。これにより、tt.Sが指すオブジェクトが到達不能と判断され、ファイナライザが実行されるはずです。
    • <-c: 最初のファイナライザが実行されるのを待ちます。
    • runtime.GC(): 2回目のGCを実行します。これは、最初のファイナライザが実行された後に、関連するオブジェクトが完全に解放されることを確認するためです。
    • <-c: 2番目のファイナライザが実行されるのを待ちます。

このテストが成功するということは、foo()関数から戻った後、GCが適切にオブジェクトを回収し、設定された両方のファイナライザが期待通りに実行されたことを意味します。もしバグが修正されていなければ、ファイナライザが実行されず、チャネルからの受信がブロックされることになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(runtimeパッケージ、ガベージコレクションに関する情報)
  • Go言語のソースコード(src/pkg/runtime/mgc0.csrc/pkg/runtime/malloc.goなど)
  • Go言語のIssueトラッカー(Issue 5348の詳細な議論)
  • Go言語のガベージコレクションに関する技術記事やブログポスト
  • Goのruntime.SetFinalizerに関する解説記事
  • Goのメモリ管理に関する書籍やオンラインリソース