あおかびのおすなば

日々のコトとかなんか見た感想とか

Goで循環参照なオブジェクトはGCはされるのか

要約

Goでオブジェクトの循環参照を作ったときGCはされるのか.
マークアンドスイープなのでされる.
実験で確かめた.

こんにちは

この記事はKMCアドベントカレンダーの14日目の記事です.
adventar.org

昨日はdefinedさんの,素人が1から音ゲー曲を作る話【KMCアドベントカレンダー2021】 - でぃふぁいんどの空間でした.
ローパスフィルタっぽいところとか音ゲーっぽくてよかった.
defined.hatenadiary.com


この記事では,Goでオブジェクトの循環参照を作ったときGCで回収されるのかを書きます.
GCの細かいアルゴリズムの話はしません(できない).
実験してGCの動作を確かめたよ,という話です.

循環参照しているコード

Goではパッケージの循環参照はできません.
ですがオブジェクト同士の循環参照はできます.

package main

type S1 struct {
    s2 *S2
}

type S2 struct {
    s1 *S1
}

func main(){
  s1 := S1{}
  s2 := S2{}
  s1.s2 = &s2
  s2.s1 = &s1
}

s1,s2のオブジェクトがお互いを参照しています.

GoのGC

上記のコードですが,仮にs1,s2がヒープにいて,参照カウントでGCしてたら回収されなさそうです.*1

実際にはGoのGCの実装方式はmark-and-sweep方式で,上記のようなコードでも問題なく回収されます.
goのレポジトリのコメントにmark-and-sweep方式であることが書かれています.
mark-and-sweep方式の解説は調べてください.
簡単な理解だと「ルートから探索して到達できなくなったオブジェクトはGCで回収される」という方式で,
循環参照していてもルートから到達不可能であれば回収されます.

実験

もうわかったようなものですが,実際に動かして確認してみます.
実験に使ったコードです.

package main

import (
	"fmt"
	"runtime"
)

type S1 struct {
	s2 *S2
}

type S2 struct {
	s1 *S1
}

func main() {
	m1 := runtime.MemStats{}
	runtime.ReadMemStats(&m1)
	
	for i := 0; i < 10000; i++ {
		Func()
	}
	// runtime.GC()
        m2 := runtime.MemStats{}
	runtime.ReadMemStats(&m2)
	fmt.Println("before GC: ", m1.HeapObjects)
	fmt.Println("after  GC: ", m2.HeapObjects)
	return
}

func Func() {
	s1 := S1{}
	s2 := S2{}
	s1.s2 = &s2
	s2.s1 = &s1
	fmt.Println(s1.s2)
}

GCに回収されたかどうかはGC前後のヒープサイズを比較して確かめることにしました.
メモリに関する情報はruntime.ReadMemStatsで取得することができます.
MemStats.HeapObjectsはヒープに割り当てられたオブジェクトの数です.
今回はこの数値を比較します.

さてまず循環参照しているオブジェクトをヒープに置く必要があります.
go build -gcflags '-m' main.goとするとヒープとスタックどちらに置かれるか確かめることができます.

./main.go:32:2: moved to heap: s1
./main.go:33:2: moved to heap: s2

ちゃんとヒープに配置されています.
ちなみにfmt.Printlnにs1を渡さないとスタックに配置されます.

あとはGC前後のヒープサイズを見れば,GCで回収されたのか分かりそうです.

ループでFuncを繰り返し実行しています.
GC前のヒープサイズ-GC後のヒープサイズが実行するたびに若干ブレるため,変化が分かりやすいようにそうしています.

GCを実行しなかった場合の結果

...
before GC:  151
after  GC:  20156

GCを実行した場合の結果

before GC:  146
after  GC:  148

GCを実行したときはヒープサイズが小さくなっています.
これで無事循環参照もGCによって回収されることが確認できました.
めでたし.

まとめ

Goでは循環参照なオブジェクトもGCされることを実験して確かめました.
runtimeパッケージが便利ですね.
プログラムを計測する際に色々使えそうです.
これ書いてる途中で気になることも出たのでもうちょっと色々調べたかったけど,時間が来たので終わりです.

明日以降のKMCアドベントカレンダーもお楽しみに.

*1:GC=参照カウントという認識しかなかった