[インデックス 18549] ファイルの概要
このコミットは、Goランタイムのtimerproc
関数におけるメモリリークの可能性を解消することを目的としています。具体的には、タイマー関数とその引数が、次のタイマーが実行されるまでのスリープ中に適切にクリアされないことで発生するオブジェクトの参照保持を防ぎ、ガベージコレクションによる回収を可能にします。
コミット
commit be1c71ecb5b02220b282884317908b4da89ef37a
Author: Russ Cox <rsc@golang.org>
Date: Mon Feb 17 20:11:53 2014 -0500
runtime: clear f, arg to avoid leak in timerproc
I have seen this cause leaks where not all objects in a sync.Pool
would be reclaimed during the sync package tests.
I found it while debugging the '0 of 100 finalized' failure we are
seeing on arm, but it seems not to be the root cause for that one.
LGTM=dave, dvyukov
R=golang-codereviews, dave, dvyukov
CC=golang-codereviews
https://golang.org/cl/64920044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/be1c71ecb5b02220b282884317908b4da89ef37a
元コミット内容
runtime: clear f, arg to avoid leak in timerproc
このコミットは、timerproc
におけるリークを避けるために、関数f
と引数arg
をクリアします。
sync.Pool
のテスト中に、すべてのオブジェクトが回収されないリークが発生するのを確認しました。
これは、armアーキテクチャで発生している「0 of 100 finalized」エラーのデバッグ中に発見されましたが、その根本原因ではないようです。
変更の背景
このコミットの背景には、Goランタイムにおけるメモリ管理の最適化と、特定の条件下でのメモリリークの解消という重要な課題がありました。コミットメッセージによると、主な動機は以下の2点です。
sync.Pool
テストにおけるオブジェクトの回収漏れ:sync.Pool
は、オブジェクトの再利用を促進し、ガベージコレクションの負荷を軽減するためのメカニズムです。しかし、テスト中にsync.Pool
内のオブジェクトが完全に回収されないリークが確認されました。これは、timerproc
がタイマー関数とその引数を保持し続けることで、これらのオブジェクトへの参照が残り、ガベージコレクタがそれらを解放できないために発生していました。- ARMアーキテクチャでの「0 of 100 finalized」エラーのデバッグ: ARMアーキテクチャで発生していた「0 of 100 finalized」というエラーは、ファイナライザが期待通りに実行されない、またはオブジェクトが最終化されない問題を示唆していました。このコミットの変更は、この問題の直接的な原因ではなかったものの、デバッグの過程で
timerproc
におけるメモリリークの可能性が発見されました。
これらの問題は、Goプログラムの長期的な安定性とメモリ効率に影響を与える可能性があったため、timerproc
がタイマー実行後に不要な参照を保持しないように修正する必要がありました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とメモリ管理の仕組みについて理解しておく必要があります。
1. timerproc
timerproc
は、Goランタイム内部で動作する特別なゴルーチン(Goの軽量スレッド)です。その主な役割は、time.AfterFunc
やtime.NewTimer
などで設定されたすべてのタイマーを管理し、期限が来たタイマーのコールバック関数を実行することです。
- タイマーの管理:
timerproc
は、タイマーを効率的に管理するために、最小ヒープ(min-heap)データ構造を使用します。これにより、次に期限が来るタイマーを素早く特定できます。 - 実行ループ:
timerproc
は継続的にタイマーヒープを監視し、期限が来たタイマーがあれば、そのタイマーに関連付けられた関数を実行します。タイマーがすぐに期限切れにならない場合、timerproc
はnotetsleepg
やgopark
のようなメカニズムを使用して自身をスリープさせ、新しいタイマーが追加されたり、既存のタイマーが期限切れになったりしたときにウェイクアップされます。 - Go 1.14以降の変更: Go 1.14以降では、明示的な
timerproc
の非同期タスクは削除され、タイマーのスケジューリングと実行の責任はGoスケジューラのメインループまたはシステム監視に直接統合されました。これにより、コンテキストスイッチのオーバーヘッドが削減され、タイマーの応答性が向上しました。ただし、このコミットが作成された時点(Go 1.2以前)では、timerproc
は独立したゴルーチンとして存在していました。
2. sync.Pool
sync.Pool
は、Goの標準ライブラリsync
パッケージが提供する型で、一時的なオブジェクトの再利用を目的としています。
- 目的:
sync.Pool
の主な目的は、頻繁に作成および破棄される一時的なオブジェクトのメモリ割り当てとガベージコレクションのオーバーヘッドを削減することです。これにより、アプリケーションのパフォーマンスが向上し、GCポーズが短縮されます。 - 動作:
sync.Pool
は、Get()
メソッドでオブジェクトを取得し、Put()
メソッドでオブジェクトをプールに戻します。プールに戻されたオブジェクトは、将来のGet()
呼び出しで再利用される可能性があります。 - GCとの連携:
sync.Pool
はGoのガベージコレクタと連携して動作します。GCサイクルが開始される前に、ランタイムはsync.Pool
インスタンスを追跡するallPools
スライスをクリアするクリーンアッププロセスを開始します。オブジェクトは「victim area」に移動され、2回のGCサイクル後に完全に破棄されます。これにより、プールが無限に成長するのを防ぎます。 - 注意点:
sync.Pool
はキャッシュメカニズムではなく、一時的なオブジェクトの再利用に特化しています。オブジェクトをGet()
で取得した後、Put()
でプールに戻し忘れると、メモリ使用量が増加し、リークのように見えることがあります。また、再利用する前にオブジェクトの内部状態を適切にリセットする必要があります。
3. Goのガベージコレクション (GC)
Goのメモリ管理は、高度なガベージコレクタ(GC)によって自動化されています。
- スタックとヒープ: Goプログラムは、スタックとヒープの2つの主要な場所にメモリを割り当てます。
- スタックメモリ: ローカル変数や関数呼び出しデータに使用され、関数が終了すると自動的に解放されます。高速です。
- ヒープメモリ: コンパイル時にサイズが不明な動的に割り当てられたデータや、関数のライフサイクルを超えて存続する必要があるデータに使用されます。ガベージコレクタによって管理されます。
- エスケープ解析: Goのコンパイラは、エスケープ解析という技術を使用して、変数がスタックに割り当てられるべきか、ヒープに割り当てられるべきかを決定します。
- GCアルゴリズム: GoのGCは、並行、トライカラー、マーク&スイープアルゴリズムを採用しています。
- 並行: GCのほとんどの作業はアプリケーションと並行して実行され、「ストップ・ザ・ワールド」(STW)ポーズ(GC操作のためにアプリケーションの実行が一時的に停止する短い時間)を最小限に抑えます。
- トライカラーマーキング: オブジェクトを白(未マーク、回収候補)、灰色(訪問済みだが参照先未スキャン)、黒(訪問済み、参照先もスキャン済み、到達可能)の3色に分類します。
- マーク&スイープ: マーキングフェーズで到達可能なオブジェクト(黒)を特定した後、スイープフェーズで未マーク(白)のオブジェクトが占めるメモリを回収します。GoのGCは非世代別(オブジェクトを年齢で分類しない)であり、非コンパクション(メモリ内のオブジェクトを再配置しない)です。
- ファイナライザ: Goでは、
runtime.SetFinalizer
関数を使用して、オブジェクトがガベージコレクションされる直前に実行されるファイナライザを設定できます。これは、外部リソースの解放などのクリーンアップ操作に使用されます。コミットメッセージの「0 of 100 finalized」は、ファイナライザの実行に関する問題を示唆しています。
技術的詳細
このコミットの技術的詳細は、timerproc
がタイマー関数を実行した後、その関数と引数への参照を明示的にnil
に設定することで、メモリリークを防ぐという点に集約されます。
timerproc
は、タイマーが期限切れになったときに実行される関数f
と、その関数に渡される引数arg
を内部的に保持しています。タイマーが実行された後、timerproc
は次のタイマーの期限が来るまでスリープ状態に入ることがあります。このスリープ中に、もしf
とarg
がクリアされずに残っていると、それらが参照しているオブジェクト(例えば、sync.Pool
から取得されたオブジェクトや、クロージャによってキャプチャされた変数など)は、ガベージコレクタによって到達可能と判断され、回収されません。
特に、sync.Pool
のテストでリークが確認されたのは、timerproc
がsync.Pool
から取得されたオブジェクトを引数として受け取り、そのオブジェクトへの参照を保持し続けたためと考えられます。sync.Pool
は、GCサイクル中にプール内のオブジェクトをクリーンアップしますが、もし外部から参照されているオブジェクトがあれば、それは回収の対象外となります。
このコミットでは、タイマー関数f
が実行された直後に、f
自体をnil
に、そしてarg
の内部データポインタをnil
に設定しています。これにより、timerproc
がこれらのオブジェクトへの参照を解放し、ガベージコレクタがそれらを適切に回収できるようになります。
USED(f)
とUSED(&arg)
というマクロは、Goのコンパイラがこれらのnil
割り当てを最適化によって削除しないようにするためのものです。Goのコンパイラは、変数がその後使用されないと判断した場合、その変数の割り当てを削除することがあります。しかし、このケースでは、nil
を割り当てること自体がメモリリークを防ぐための重要な操作であるため、コンパイラにこの割り当てを保持させる必要があります。USED
マクロは、変数が「使用されている」とマークすることで、この最適化を防ぎます。
この修正は、timerproc
が長時間稼働するシステムにおいて、微細なメモリリークが蓄積し、最終的にメモリ使用量の増加やパフォーマンスの低下を引き起こす可能性を排除します。また、sync.Pool
のようなメモリ最適化メカニズムが意図通りに機能することを保証します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/time.goc
ファイルにあります。
--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -217,6 +217,14 @@ timerproc(void)
if(raceenabled)
runtime·raceacquire(t);
f(now, arg);
+
+ // clear f and arg to avoid leak while sleeping for next timer
+ f = nil;
+ USED(f);
+ arg.type = nil;
+ arg.data = nil;
+ USED(&arg);
+
runtime·lock(&timers);
}
if(delta < 0) {
コアとなるコードの解説
追加されたコードは以下の部分です。
// clear f and arg to avoid leak while sleeping for next timer
f = nil;
USED(f);
arg.type = nil;
arg.data = nil;
USED(&arg);
-
f = nil;
f
は、実行されたタイマーのコールバック関数へのポインタです。- この行は、関数が実行された後、
f
が保持していた関数オブジェクトへの参照をnil
に設定します。これにより、f
が参照していた関数オブジェクトがガベージコレクションの対象となり、メモリが解放される可能性が高まります。
-
USED(f);
USED
マクロは、Goランタイムの内部で使われるもので、コンパイラが変数の使用を最適化によって削除しないようにするためのヒントです。f = nil
という代入が、その後のコードでf
が使われないとコンパイラが判断した場合に削除されてしまうことを防ぎます。このnil
代入自体がメモリリークを防ぐための重要な操作であるため、削除されてはなりません。
-
arg.type = nil;
arg
は、タイマー関数f
に渡される引数です。Goランタイム内部では、引数はinterface{}
型のように扱われ、その実体はtype
とdata
の2つのフィールドで表現されます。type
フィールドは引数の型情報を保持します。- この行は、引数の型情報への参照を
nil
に設定します。
-
arg.data = nil;
data
フィールドは、引数の実際の値(データ)へのポインタを保持します。- この行は、引数のデータへの参照を
nil
に設定します。これにより、arg
が参照していた実際のデータオブジェクトがガベージコレクションの対象となります。
-
USED(&arg);
USED(f)
と同様に、arg
のフィールドへのnil
代入がコンパイラによって削除されないようにするためのヒントです。arg
は構造体であるため、そのアドレス&arg
をUSED
に渡しています。
これらの変更により、timerproc
がタイマー関数を実行し終えた後、その関数と引数に関連するオブジェクトへの参照を積極的に解放します。これにより、これらのオブジェクトがガベージコレクタによって適切に回収され、特にsync.Pool
のような再利用メカニズムと組み合わせた場合に発生する可能性のあるメモリリークが防止されます。
関連リンク
参考にした情報源リンク
- Go runtime timerproc - gopheracademy.com
- Go runtime timerproc - hcyhj.cn
- Go runtime timerproc - sobyte.net
- Go runtime timerproc - cnblogs.com
- Go sync.Pool memory leak - victoriametrics.com
- Go sync.Pool memory leak - reliasoftware.com
- Go sync.Pool memory leak - medium.com
- Go 0 of 100 finalized failure arm - Web search result
- Go runtime memory management garbage collection - leapcell.io
- Go runtime memory management garbage collection - twilio.com
- Go runtime memory management garbage collection - medium.com
- Go runtime memory management garbage collection - hackernoon.com
- Go runtime memory management garbage collection - go101.org
- Go runtime memory management garbage collection - dev.to
- Go runtime memory management garbage collection - golang.org
- Go runtime memory management garbage collection - railway.app
- Go runtime memory management garbage collection - go-cookbook.com