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

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

このコミットは、Goランタイムが未使用のメモリをオペレーティングシステム(OS)に解放するメカニズムを導入するものです。具体的には、Goのヒープマネージャが管理するメモリ領域のうち、一定期間使用されていないものをOSに返却することで、システム全体のメモリ効率を向上させます。

変更されたファイルは以下の通りです。

  • src/pkg/runtime/malloc.h: メモリ統計 (MStats) およびメモリブロック (MSpan) の構造体に新しいフィールドを追加し、メモリ解放に関連する関数のプロトタイプを宣言しています。
  • src/pkg/runtime/mem.go: Go言語側からアクセス可能なメモリ統計 (MemStats) 構造体を更新し、OSに解放されたメモリ量などの新しい統計情報を反映させています。
  • src/pkg/runtime/mgc0.c: ガベージコレクション(GC)のスイープフェーズにおいて、新しく未使用になったメモリブロック (MSpan) にタイムスタンプを付与するロジックを追加しています。また、GCが最後に実行された時刻を記録するよう変更しています。
  • src/pkg/runtime/mheap.c: Goランタイムのヒープ管理の中核をなすファイルです。メモリ解放の主要なロジックである「スカベンジャー」ゴルーチン (runtime·MHeap_Scavenger) が実装されています。このスカベンジャーは、未使用のメモリブロックを定期的にスキャンし、OSに返却する処理を行います。また、メモリブロックの割り当て、解放、結合時の新しいフィールドの管理ロジックも含まれています。
  • src/pkg/runtime/proc.c: Goランタイムのスケジューラとプロセス管理に関するファイルです。スカベンジャーゴルーチンを起動し、デッドロック検出ロジックを、スカベンジャーゴルーチンの存在を考慮するように修正しています。

コミット

commit 5c598d3c9f0b9d78f92ffe1ab5a2365fe900c631
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date:   Thu Feb 16 13:30:04 2012 -0500

    runtime: release unused memory to the OS.
    
    Periodically browse MHeap's freelists for long unused spans and release them if any.
    
    Current hardcoded settings:
            - GC is forced if none occured over the last 2 minutes.
            - spans are handed back after 5 minutes of uselessness.
    
    SysUnused (for Unix) is a wrapper on madvise MADV_DONTNEED on Linux and MADV_FREE on BSDs.
    
    R=rsc, dvyukov, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5451057

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

https://github.com/golang/go/commit/5c598d3c9f0b9d78f92ffe1ab5a2365fe900c631

元コミット内容

commit 5c598d3c9f0b9d78f92ffe1ab5a2365fe900c631
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date:   Thu Feb 16 13:30:04 2012 -0500

    runtime: release unused memory to the OS.
    
    Periodically browse MHeap's freelists for long unused spans and release them if any.
    
    Current hardcoded settings:
            - GC is forced if none occured over the last 2 minutes.
            - spans are handed back after 5 minutes of uselessness.
    
    SysUnused (for Unix) is a wrapper on madvise MADV_DONTNEED on Linux and MADV_FREE on BSDs.
    
    R=rsc, dvyukov, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5451057

変更の背景

Goランタイムは、プログラムが使用するメモリを効率的に管理するために独自のヒープとガベージコレクタ(GC)を持っています。しかし、初期のGoランタイムでは、一度OSから確保したメモリを、Goプログラムが使用しなくなった後もOSにすぐに返却しない傾向がありました。これは、将来の割り当てのためにメモリを保持しておくことで、OSへのシステムコールオーバーヘッドを削減し、割り当て速度を向上させるという意図がありました。

しかし、この挙動は、特に長時間稼働するサーバーアプリケーションや、一時的に大量のメモリを使用するがその後解放するようなアプリケーションにおいて、問題を引き起こす可能性がありました。Goプログラムが実際に使用しているメモリ量は少ないにもかかわらず、OSから見ると大量のメモリを占有しているように見え(Resident Set Size: RSSが高い)、結果としてシステム全体のメモリ利用効率が低下したり、他のプロセスがメモリ不足に陥ったりする可能性がありました。

このコミットは、この問題を解決するために、Goランタイムが不要になったメモリをOSに積極的に返却するメカニズムを導入することを目的としています。これにより、Goアプリケーションのメモリフットプリントを削減し、より良いシステムリソースの利用を促進します。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念を把握しておく必要があります。

Goランタイムのメモリ管理

Goランタイムは、独自のメモリヒープを管理しています。このヒープは、OSから大きなメモリチャンク(アリーナ)を確保し、それをより小さな単位に分割してGoプログラムのオブジェクト割り当てに利用します。

  • MHeap: Goランタイム全体のヒープを表す構造体です。OSから確保したメモリを管理します。
  • MSpan: Goランタイムのメモリ管理の基本的な単位です。連続したページ(通常は4KB)の集合を表します。オブジェクトのサイズクラスに応じて、異なるサイズのMSpanが割り当てられます。MSpanは、使用中(MSpanInUse)、アイドル状態(MSpanFree)、またはOSに返却済み(MSpanDead)などの状態を持ちます。
  • MStats: Goランタイムのメモリ使用状況に関する統計情報を保持する構造体です。ヒープの割り当て量、OSから確保した量、アイドル状態のメモリ量などが含まれます。

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

Goはトレース型ガベージコレクタを採用しており、プログラムが参照しなくなったメモリを自動的に解放します。GoのGCは主に「マーク&スイープ」アルゴリズムに基づいています。

  • マークフェーズ: プログラムが到達可能なオブジェクトを特定し、マークします。
  • スイープフェーズ: マークされなかったオブジェクトが占有していたメモリを解放し、再利用可能な状態にします。このコミットでは、スイープフェーズで新しく未使用になったMSpanにタイムスタンプを付与する変更が加えられています。

仮想メモリと物理メモリ

  • 仮想メモリ: 各プロセスが利用できるメモリ空間の抽象化です。プロセスは連続した仮想アドレス空間を見ますが、これは必ずしも物理メモリ上の連続した領域に対応しません。
  • 物理メモリ: コンピュータに実際に搭載されているRAMのことです。
  • Resident Set Size (RSS): プロセスが現在物理メモリ上に保持しているメモリの量です。GoランタイムがOSから確保したメモリを返却しない場合、たとえGoプログラムがそのメモリを使用していなくても、RSSは高いままになることがあります。

madvise システムコール

madviseはUnix系OSで利用可能なシステムコールで、アプリケーションがカーネルに対して、特定のメモリ領域の利用パターンに関するアドバイスを与えるために使用されます。これにより、カーネルはメモリ管理の最適化を行うことができます。

このコミットで特に重要なのは以下のフラグです。

  • MADV_DONTNEED (Linux): 指定されたメモリ領域の内容は、もはやプロセスによって必要とされないことをカーネルに伝えます。カーネルは、そのページを物理メモリから解放し、スワップアウトしたり、他の用途に再利用したりすることができます。次にそのメモリ領域にアクセスがあった場合、ページフォルトが発生し、必要に応じてページが再ロードされます(通常はゼロフィルされます)。
  • MADV_FREE (FreeBSD, macOS): MADV_DONTNEEDに似ていますが、より積極的な解放を意味します。指定されたメモリ領域は、将来的にアクセスされる可能性が低いことを示唆します。カーネルは、そのページを物理メモリから解放できますが、もしそのページが変更されていなければ、再利用されるまでその内容を保持する可能性があります。変更されたページは、MADV_DONTNEEDと同様に扱われます。

このコミットでは、SysUnusedというGoランタイム内部のラッパー関数を通じて、これらのシステムコールが利用されます。

Goroutine

Goの軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のGoroutineを同時に実行できます。このコミットでは、メモリ解放処理をバックグラウンドで実行するために新しいGoroutine(スカベンジャー)が導入されます。

技術的詳細

このコミットの主要な技術的変更は、Goランタイムに「スカベンジャー」と呼ばれる新しいバックグラウンドゴルーチンを導入し、未使用のメモリをOSに返却するメカニズムを実装した点です。

  1. スカベンジャーゴルーチンの導入:

    • runtime·MHeap_Scavengerという新しいC関数が実装され、これが独立したゴルーチンとしてruntime·schedinit(スケジューラの初期化時)に起動されます。
    • このゴルーチンは無限ループで動作し、定期的にスリープとウェイクアップを繰り返します。
    • ウェイクアップ周期は、強制GCの閾値(2分)とメモリ解放の閾値(5分)に基づいて設定されます。
  2. 強制GCのメカニズム:

    • スカベンジャーゴルーチンは、前回のGCから2分以上経過している場合、強制的にGCを実行します。これは、メモリ解放の前提として、まずGCによって不要なオブジェクトが回収され、MSpanがアイドル状態になる必要があるためです。
    • mstats.last_gcという新しいフィールドが導入され、GCが最後に実行された絶対時刻を記録します。
  3. 未使用MSpanの追跡:

    • MSpan構造体にunusedsincenpreleasedという新しいフィールドが追加されました。
      • unusedsince (int64): MSpanがMSpanFree(アイドル状態)になった最初の時刻を記録します。GCのスイープフェーズ (mgc0.csweep関数内) で、新しくアイドル状態になったMSpanに現在の時刻がスタンプされます。
      • npreleased (uintptr): そのMSpan内で既にOSに返却されたページ数を記録します。これにより、MSpan全体を返却するのではなく、部分的に返却された状態を追跡できます。
  4. メモリ解放のロジック:

    • スカベンジャーゴルーチンは、MHeapのフリーリスト(h->freeh->large)を走査します。
    • 各MSpanについて、unusedsinceが設定されており、かつnow - s->unusedsince > limit(現在時刻からunusedsinceまでの経過時間が5分を超えている)という条件を満たす場合、そのMSpanはOSに返却する対象となります。
    • 返却されるメモリ量は、s->npages - s->npreleased(総ページ数から既に返却済みのページ数を引いたもの)に基づいて計算されます。
    • runtime·SysUnused関数が呼び出され、実際のOSへのメモリ返却(madviseシステムコール)が行われます。
    • mstats.heap_releasedという新しい統計情報が更新され、OSに返却された総メモリ量を追跡します。
    • MSpanがOSに返却された後、s->npreleaseds->npagesに設定され、そのMSpanの全ページが返却済みであることを示します。
  5. メモリ再利用時の処理:

    • MHeap_AllocLocked(メモリ割り当て時)では、再利用されるMSpanのmstats.heap_releasedから以前に返却されたメモリ量を減算し、s->npreleasedをリセットします。
    • MHeap_FreeLocked(メモリ解放時)では、MSpanがフリーリストに戻される際にunusedsincenpreleasedをリセットします。また、隣接するMSpanが結合される際には、npreleasedの値も適切に結合されます。
  6. デッドロック検出の修正:

    • Goランタイムには、すべてのゴルーチンがスリープ状態になり、実行可能なゴルーチンが存在しない場合にデッドロックと判断するメカニズムがあります。
    • スカベンジャーゴルーチンは定期的にスリープするため、このゴルーチンだけが残っている場合に誤ってデッドロックと判断されないよう、proc.cのデッドロック検出ロジックが修正されました。scvg(スカベンジャーゴルーチンのポインタ)がGrunningまたはGsyscall状態の場合、デッドロックとは見なされません。

この一連の変更により、Goランタイムはアイドル状態のメモリをより積極的にOSに返却し、システム全体のメモリ効率を向上させることが可能になりました。

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

このコミットのコアとなる変更は、主にsrc/pkg/runtime/mheap.cに実装されたruntime·MHeap_Scavenger関数と、メモリ統計およびMSpan構造体への新しいフィールドの追加です。

src/pkg/runtime/malloc.h

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -205,6 +205,7 @@ struct MStats
 	uint64	heap_sys;	// bytes obtained from system
 	uint64	heap_idle;	// bytes in idle spans
 	uint64	heap_inuse;	// bytes in non-idle spans
+	uint64	heap_released;	// bytes released to the OS
 	uint64	heap_objects;	// total number of allocated objects
 
 	// Statistics about allocation of low-level fixed-size structures.
@@ -220,6 +221,7 @@ struct MStats
 	// Statistics about garbage collector.
 	// Protected by stopping the world during GC.
 	uint64	next_gc;	// next GC (in heap_alloc time)
+	uint64  last_gc;	// last GC (in absolute time)
 	uint64	pause_total_ns;
 	uint64	pause_ns[256];
 	uint32	numgc;
@@ -304,14 +306,16 @@ struct MSpan
 {
 	MSpan	*next;		// in a span linked list
 	MSpan	*prev;		// in a span linked list
-	MSpan	*allnext;		// in the list of all spans
+	MSpan	*allnext;	// in the list of all spans
 	PageID	start;		// starting page number
 	uintptr	npages;		// number of pages in span
 	MLink	*freelist;	// list of free objects
 	uint32	ref;		// number of allocated objects in this span
 	uint32	sizeclass;	// size class
 	uint32	state;		// MSpanInUse etc
-	byte	*limit;	// end of data in span
+	int64   unusedsince;	// First time spotted by GC in MSpanFree state
+	uintptr npreleased;	// number of pages released to the OS
+	byte	*limit;		// end of data in span
 };
 
 void	runtime·MSpan_Init(MSpan *span, PageID start, uintptr npages);
@@ -381,6 +385,7 @@ MSpan*	runtime·MHeap_LookupMaybe(MHeap *h, void *v);\n void	runtime·MGetSizeClassInfo(int32 sizeclass, uintptr *size, int32 *npages, int32 *nobj);\n void*	runtime·MHeap_SysAlloc(MHeap *h, uintptr n);\n void	runtime·MHeap_MapBits(MHeap *h);\n+void	runtime·MHeap_Scavenger(void);\n \n void*	runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed);\n int32	runtime·mlookup(void *v, byte **base, uintptr *size, MSpan **s);\

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -716,8 +716,10 @@ sweep(void)
 	byte *p;
 	MCache *c;
 	byte *arena_start;
+\tint64 now;
 
 	arena_start = runtime·mheap.arena_start;
+\tnow = runtime·nanotime();
 
 	for(;;) {
 		s = work.spans;
@@ -726,6 +728,11 @@ sweep(void)
 		if(!runtime·casp(&work.spans, s, s->allnext))
 			continue;
 
+		// Stamp newly unused spans. The scavenger will use that
+		// info to potentially give back some pages to the OS.
+		if(s->state == MSpanFree && s->unusedsince == 0)
+			s->unusedsince = now;
+
 		if(s->state != MSpanInUse)
 			continue;
 
@@ -963,6 +970,7 @@ runtime·gc(int32 force)
 	obj1 = mstats.nmalloc - mstats.nfree;
 
 	t3 = runtime·nanotime();
+\tmstats.last_gc = t3;
 	mstats.pause_ns[mstats.numgc%nelem(mstats.pause_ns)] = t3 - t0;
 	mstats.pause_total_ns += t3 - t0;
 	mstats.numgc++;

src/pkg/runtime/mheap.c

--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -103,6 +103,8 @@ HaveSpan:
 	runtime·MSpanList_Remove(s);
 	s->state = MSpanInUse;
 	mstats.heap_idle -= s->npages<<PageShift;
+\tmstats.heap_released -= s->npreleased<<PageShift;
+\ts->npreleased = 0;
 
 	if(s->npages > npage) {
 		// Trim extra and put it back in the heap.
@@ -280,6 +282,8 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)
 	}
 	mstats.heap_idle += s->npages<<PageShift;
 	s->state = MSpanFree;
+\ts->unusedsince = 0;
+\ts->npreleased = 0;
 	runtime·MSpanList_Remove(s);
 	sp = (uintptr*)(s->start<<PageShift);\
 
@@ -292,6 +296,7 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)
 		*tp |= *sp;	// propagate "needs zeroing" mark
 		s->start = t->start;
 		s->npages += t->npages;
+\t\ts->npreleased = t->npreleased; // absorb released pages
 		p -= t->npages;
 		h->map[p] = s;
 		runtime·MSpanList_Remove(t);
@@ -304,6 +309,7 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)
 		tp = (uintptr*)(t->start<<PageShift);
 		*sp |= *tp;	// propagate "needs zeroing" mark
 		s->npages += t->npages;
+\t\ts->npreleased += t->npreleased;
 		h->map[p + s->npages - 1] = s;
 		runtime·MSpanList_Remove(t);
 		t->state = MSpanDead;
@@ -317,8 +323,81 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)
 	else
 		runtime·MSpanList_Insert(&h->large, s);
+}\
 
-\t// TODO(rsc): IncrementalScavenge() to return memory to OS.\n+// Release (part of) unused memory to OS.\n+// Goroutine created in runtime·schedinit.\n+// Loop forever.\n+void\n+runtime·MHeap_Scavenger(void)\n+{\n+\tMHeap *h;\n+\tMSpan *s, *list;\n+\tuint64 tick, now, forcegc, limit;\n+\tuint32 k, i;\n+\tuintptr released, sumreleased;\n+\tbyte *env;\n+\tbool trace;\n+\tNote note;\n+\n+\t// If we go two minutes without a garbage collection, force one to run.\n+\tforcegc = 2*60*1e9;\n+\t// If a span goes unused for 5 minutes after a garbage collection,\n+\t// we hand it back to the operating system.\n+\tlimit = 5*60*1e9;\n+\t// Make wake-up period small enough for the sampling to be correct.\n+\ttick = forcegc < limit ? forcegc/2 : limit/2;\n+\n+\ttrace = false;\n+\tenv = runtime·getenv(\"GOGCTRACE\");\n+\tif(env != nil)\n+\t\ttrace = runtime·atoi(env) > 0;\n+\n+\th = &runtime·mheap;\n+\tfor(k=0;; k++) {\n+\t\truntime·noteclear(&note);\n+\t\truntime·entersyscall();\n+\t\truntime·notetsleep(&note, tick);\n+\t\truntime·exitsyscall();\n+\n+\t\truntime·lock(h);\n+\t\tnow = runtime·nanotime();\n+\t\tif(now - mstats.last_gc > forcegc) {\n+\t\t\truntime·unlock(h);\n+\t\t\truntime·gc(1);\n+\t\t\truntime·lock(h);\n+\t\t\tnow = runtime·nanotime();\n+\t\t\tif (trace)\n+\t\t\t\truntime·printf(\"scvg%d: GC forced\\n\", k);\n+\t\t}\n+\t\tsumreleased = 0;\n+\t\tfor(i=0; i < nelem(h->free)+1; i++) {\n+\t\t\tif(i < nelem(h->free))\n+\t\t\t\tlist = &h->free[i];\n+\t\t\telse\n+\t\t\t\tlist = &h->large;\n+\t\t\tif(runtime·MSpanList_IsEmpty(list))\n+\t\t\t\tcontinue;\n+\t\t\tfor(s=list->next; s != list; s=s->next) {\n+\t\t\t\tif(s->unusedsince != 0 && (now - s->unusedsince) > limit) {\n+\t\t\t\t\treleased = (s->npages - s->npreleased) << PageShift;\n+\t\t\t\t\tmstats.heap_released += released;\n+\t\t\t\t\tsumreleased += released;\n+\t\t\t\t\ts->npreleased = s->npages;\n+\t\t\t\t\truntime·SysUnused((void*)(s->start << PageShift), s->npages << PageShift);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\truntime·unlock(h);\n+\n+\t\tif(trace) {\n+\t\t\tif(sumreleased > 0)\n+\t\t\t\truntime·printf(\"scvg%d: %p MB released\\n\", k, sumreleased>>20);\n+\t\t\truntime·printf(\"scvg%d: inuse: %D, idle: %D, sys: %D, released: %D, consumed: %D (MB)\\n\",\n+\t\t\t\tk, mstats.heap_inuse>>20, mstats.heap_idle>>20, mstats.heap_sys>>20,\n+\t\t\t\tmstats.heap_released>>20, (mstats.heap_sys - mstats.heap_released)>>20);\n+\t\t}\n+\t}\n }\n \n // Initialize a new span with the given start and npages.\
@@ -333,6 +412,8 @@ runtime·MSpan_Init(MSpan *span, PageID start, uintptr npages)\n 	span->ref = 0;\n 	span->sizeclass = 0;\n 	span->state = 0;\n+\tspan->unusedsince = 0;\n+\tspan->npreleased = 0;\n }\n \n // Initialize an empty doubly-linked list.\

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -164,6 +164,9 @@ setmcpumax(uint32 n)\n 	}\n }\n \n+// Keep trace of scavenger's goroutine for deadlock detection.\n+static G *scvg;\n+\n // The bootstrap sequence is:\n //\n //	call osinit
@@ -206,6 +209,8 @@ runtime·schedinit(void)\n \n 	mstats.enablegc = 1;\n 	m->nomemprof--;\n+\n+\tscvg = runtime·newproc1((byte*)runtime·MHeap_Scavenger, nil, 0, 0, runtime·schedinit);\n }\n \n extern void main·init(void);\
@@ -582,9 +587,12 @@ top:\n 		mput(m);\n 	}\n \n-\tv = runtime·atomicload(&runtime·sched.atomic);\n-\tif(runtime·sched.grunning == 0)\n-\t\truntime·throw(\"all goroutines are asleep - deadlock!\");\n+\t// Look for deadlock situation: one single active g which happens to be scvg.\n+\tif(runtime·sched.grunning == 1 && runtime·sched.gwait == 0) {\n+\t\tif(scvg->status == Grunning || scvg->status == Gsyscall)\n+\t\t\truntime·throw(\"all goroutines are asleep - deadlock!\");\n+\t}\n+\n \tm->nextg = nil;\n \tm->waitnextg = 1;\n \truntime·noteclear(&m->havenextg);\
@@ -593,6 +601,7 @@ top:\n 	// it will see the waitstop and take the slow path.\n 	// Exitsyscall never increments mcpu beyond mcpumax.\n+\tv = runtime·atomicload(&runtime·sched.atomic);\n \tif(atomic_waitstop(v) && atomic_mcpu(v) <= atomic_mcpumax(v)) {\n \t\t// set waitstop = 0 (known to be 1)\n \t\truntime·xadd(&runtime·sched.atomic, -1<<waitstopShift);\

コアとなるコードの解説

このコミットの核心は、src/pkg/runtime/mheap.cに新しく追加されたruntime·MHeap_Scavenger関数です。

void
runtime·MHeap_Scavenger(void)
{
	MHeap *h;
	MSpan *s, *list;
	uint64 tick, now, forcegc, limit;
	uint32 k, i;
	uintptr released, sumreleased;
	byte *env;
	bool trace;
	Note note;

	// If we go two minutes without a garbage collection, force one to run.
	forcegc = 2*60*1e9; // 2 minutes in nanoseconds
	// If a span goes unused for 5 minutes after a garbage collection,
	// we hand it back to the operating system.
	limit = 5*60*1e9; // 5 minutes in nanoseconds
	// Make wake-up period small enough for the sampling to be correct.
	tick = forcegc < limit ? forcegc/2 : limit/2; // Scavenger wakes up every 1 or 2.5 minutes

	trace = false;
	env = runtime·getenv("GOGCTRACE");
	if(env != nil)
		trace = runtime·atoi(env) > 0;

	h = &runtime·mheap;
	for(k=0;; k++) { // Infinite loop for the scavenger goroutine
		runtime·noteclear(&note);
		runtime·entersyscall();
		runtime·notetsleep(&note, tick); // Sleep for 'tick' duration
		runtime·exitsyscall();

		runtime·lock(h); // Lock the heap for safe access
		now = runtime·nanotime(); // Get current time

		// Force GC if no GC occurred for 'forcegc' duration
		if(now - mstats.last_gc > forcegc) {
			runtime·unlock(h);
			runtime·gc(1); // Force a GC cycle
			runtime·lock(h);
			now = runtime·nanotime();
			if (trace)
				runtime·printf("scvg%d: GC forced\\n", k);
		}

		sumreleased = 0;
		// Iterate through all free lists (small and large spans)
		for(i=0; i < nelem(h->free)+1; i++) {
			if(i < nelem(h->free))
				list = &h->free[i];
			else
				list = &h->large; // Large spans list

			if(runtime·MSpanList_IsEmpty(list))
				continue;

			// Iterate through spans in the current free list
			for(s=list->next; s != list; s=s->next) {
				// Check if span has been unused for 'limit' duration
				if(s->unusedsince != 0 && (now - s->unusedsince) > limit) {
					released = (s->npages - s->npreleased) << PageShift; // Calculate bytes to release
					mstats.heap_released += released; // Update total released stats
					sumreleased += released; // Update sum for this cycle
					s->npreleased = s->npages; // Mark all pages in this span as released
					// Call SysUnused to advise OS to release memory
					runtime·SysUnused((void*)(s->start << PageShift), s->npages << PageShift);
				}
			}
		}
		runtime·unlock(h); // Unlock the heap

		if(trace) { // Print tracing information if GOGCTRACE is enabled
			if(sumreleased > 0)
				runtime·printf("scvg%d: %p MB released\\n", k, sumreleased>>20);
			runtime·printf("scvg%d: inuse: %D, idle: %D, sys: %D, released: %D, consumed: %D (MB)\\n",
				k, mstats.heap_inuse>>20, mstats.heap_idle>>20, mstats.heap_sys>>20,
				mstats.heap_released>>20, (mstats.heap_sys - mstats.heap_released)>>20);
		}
	}
}

この関数は、Goランタイムの起動時に独立したゴルーチンとして実行されます。その主な役割は以下の通りです。

  1. 定期的なウェイクアップ: runtime·notetsleepを使用して、設定されたtick間隔(強制GCの閾値とメモリ解放の閾値の半分)で定期的にスリープし、ウェイクアップします。これにより、バックグラウンドで継続的にメモリの状態を監視できます。
  2. 強制GCの実行: ウェイクアップするたびに、前回のGCから2分以上経過しているかをチェックします。もし経過していれば、runtime·gc(1)を呼び出して強制的にGCを実行します。これは、メモリをOSに返却する前に、まずGoヒープ内の不要なオブジェクトをGCが回収し、MSpanをアイドル状態にする必要があるためです。
  3. 未使用MSpanの走査と解放:
    • runtime·lock(h)でヒープをロックし、安全にヒープの状態にアクセスします。
    • h->free(小さなサイズのMSpanのフリーリスト)とh->large(大きなサイズのMSpanのフリーリスト)の両方を走査します。
    • 各MSpan sについて、以下の条件をチェックします。
      • s->unusedsince != 0: そのMSpanが一度でもアイドル状態になったことがあるか。
      • (now - s->unusedsince) > limit: そのMSpanがアイドル状態になってから5分以上経過しているか。
    • これらの条件を満たすMSpanが見つかった場合、そのMSpanはOSに返却する対象となります。
    • released = (s->npages - s->npreleased) << PageShift;によって、まだOSに返却されていないページ数を計算します。PageShiftはページサイズ(通常4KB)を表すビットシフト値です。
    • mstats.heap_released += released;で、OSに返却された総メモリ量を更新します。
    • s->npreleased = s->npages;で、そのMSpanの全ページがOSに返却済みであることをマークします。
    • runtime·SysUnused((void*)(s->start << PageShift), s->npages << PageShift);を呼び出し、実際のOSへのメモリ返却処理(madviseシステムコール)を実行します。
  4. 統計情報の出力: GOGCTRACE環境変数が設定されている場合、スカベンジャーの活動に関する詳細なログ(解放されたメモリ量、ヒープの状態など)を標準出力に表示します。

このスカベンジャーゴルーチンは、Goランタイムがメモリを効率的に管理し、システムリソースをより適切に利用するための重要なコンポーネントとなります。

関連リンク

参考にした情報源リンク