Skip to content

runtime: objects pointed to by package-level var initializer are not garbage collected after pointer reassignment #79587

Description

@doujianli

Go version

go version go1.25.5 darwin/arm64

Output of go env in your module/workspace:

n/a

What did you do?

Reproduction

Two functionally identical programs differ only in where the package-level pointer is initialized — inline vs. in init().

leak.go — leaks (inline initialization):

package main

type List struct {
	next *List
}

var l = &List{
	next: &List{},
}

func next() {
	next := l.next
	next.next = &List{}
	l = next
}

noleak.go — no leak (init() initialization):

package main

type List struct {
	next *List
}

var l *List

func init() {
	l = &List{
		next: &List{},
	}
}

func next() {
	next := l.next
	next.next = &List{}
	l = next
}

main.go (test driver):

package main

import (
	"fmt"
	"runtime"
	"runtime/debug"
)

func formatMB(b uint64) string {
	return fmt.Sprintf("%.2f MB", float64(b)/1024/1024)
}

func printMemStats(label string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("[%s] HeapAlloc=%s HeapSys=%s HeapObjects=%d NumGC=%d\n",
		label,
		formatMB(m.HeapAlloc),
		formatMB(m.HeapSys),
		m.HeapObjects,
		m.NumGC,
	)
}

func main() {
	printMemStats("init")
	for i := 0; i < 1_000_000; i++ {
		next()
	}
	runtime.GC()
	runtime.GC()
	debug.FreeOSMemory()
	printMemStats("after 1M next")
}

What did you see happen?

Leaking version (inline var initialization):

[init]          HeapAlloc=0.13 MB  HeapSys=3.75 MB   HeapObjects=140      NumGC=0
[after 1M next] HeapAlloc=7.77 MB  HeapSys=11.69 MB  HeapObjects=1000147  NumGC=4

Non-leaking version (init() initialization):

[init]          HeapAlloc=0.13 MB  HeapSys=3.75 MB  HeapObjects=142  NumGC=0
[after 1M next] HeapAlloc=0.14 MB  HeapSys=7.66 MB  HeapObjects=151  NumGC=5

After 1,000,000 next() calls + explicit runtime.GC() + debug.FreeOSMemory():

  • Inline initialization: 1,000,147 objects remain on the heap — all 1M discarded nodes are retained, HeapAlloc grows to 7.77 MB.
  • init() initialization: only 151 objects remain — old nodes are collected as expected, HeapAlloc stays at 0.14 MB.

Objects created during static package-level initialization appear to bypass normal escape analysis reporting. This suggests the runtime may not register them as regular GC-managed heap objects, causing them to act as pinned roots even after the pointer is reassigned.

What did you expect to see?

Both versions should produce the same heap after GC. Old StorageEvent nodes that are no longer reachable (the global pointer has been reassigned away from them) should be collected.

Each next() call:

  1. advances l to its .next
  2. allocates a new node as the new tail

The previous head node loses all references and should be eligible for collection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions