Looking at the memory map of a Go 1.27 program on Linux

Many years ago I wrote an entry about a deep dive into the Linux OS memory use of a simple Go 1.11 program . For reasons of my own, I'm returning to this in Go 1.27rc2, because these days Go can tell us much more information about memory usage. As before, I'll use this very simple program:

package main
func main() {
    var i uint64
    for {
        i++
    }
}

You'll want to set 'go 1.25' or higher in the go.mod file, and then when you run this on a suitable Linux distribution you'll get a quite informative /proc/<pid>/maps file, which I'm going to break up into sections and reformat a bit to have sizes instead of start-end runs. All of the following is from a 64-bit x86 Ubuntu 26.04 LTS server.

First we have the program's machine code, read-only data, initialized variables, and zero'd data ('bss') space:

00400000          504K r-xp   memdemo
0047e000          656K r--p   memdemo
00522000           32K rw-p   memdemo
0052a000          212K rw-p 

This is all contiguous, covering 0x00400000 to 0x0055f000. The 'r', 'w', and 'x' flags mean read, write, and code execution respectively, and the 'p' flag means that all of these mappings are copy on write ('private'). I don't know why the code and read-only data sections are mapped 'p', and I'm not going to speculate.

(If we compare to the Go 1.11 version from 2018 , it looks like the program itself has grown, probably through growth in various runtime packages that are implicitly pulled in.)

Then we have the main Go heap with its initial 64 MByte arena. This program has apparently only needed a 4 MByte chunk out of it to actually be allocated, with the rest as reserved space right now.

d75e4000000     12288K ---p  [anon: Go: heap reservation]
d75e4c00000      4096K rw-p  [anon: Go: heap]
d75e5000000     49152K ---p  [anon: Go: heap reservation]

This is all contiguous in a block from 0xd75e4000000 through 0xd75e8000000, which makes the 64 MByte nature more obvious. The Go heap's location used to be fixed, but since Go 1.26 randomizing the heap base address has been the default on 64-bit platforms . You can in theory turn it off for now because it's a lingering Go experiment ( via ).

(This is RandomizedHeapBase64 in internal/goexperiment .)

Then we have an assortment of Go internal allocations, all stacked up one after the other:

7b3b11460000      256K rw-p  [anon: Go: immortal metadata]
7b3b114a0000     1408K rw-p  [anon: Go: profiler hash buckets]
7b3b11600000    32768K rw-p  [anon: Go: heap index]
7b3b13600000   289708K ---p  [anon: Go: scavenge index]
7b3b250eb000        4K rw-p  [anon: Go: scavenge index]
7b3b250ec000   234576K ---p  [anon: Go: scavenge index]
7b3b33600000   289708K ---p  [anon: Go: page summary]
7b3b450eb000        4K rw-p  [anon: Go: page alloc]
7b3b450ec000   270788K ---p  [anon: Go: page summary]
7b3b5595d000        4K rw-p  [anon: Go: page alloc]
7b3b5595e000    33844K ---p  [anon: Go: page summary]
7b3b57a6b000        4K rw-p  [anon: Go: page alloc]
7b3b57a6c000     3664K ---p  [anon: Go: page summary]
7b3b57e1c000      512K rw-p  [anon: Go: immortal metadata]
7b3b57e9c000       64K rw-p  [anon: Go: gc bits]
7b3b57eac000       64K rw-p  [anon: Go: allspans array]
7b3b57ebc000     1024K rw-p  [anon: Go: page alloc index]
7b3b57fbc000       72K rw-p  [anon: Go: immortal metadata]
7b3b57fce000      564K ---p  [anon: Go: page summary]
7b3b5805b000        4K rw-p  [anon: Go: page alloc]
7b3b5805c000      456K ---p  [anon: Go: page summary]
7b3b580ce000      128K rw-p  [anon: Go: page alloc]
7b3b580ee000      256K rw-p  [anon: Go: immortal metadata]

This area runs from 0x7b3b11460000 through 0x7b3b5812e000, although the largest bits of it aren't accessible ('---p', they have no read, write, or code execution permissions). This is a bit over a gigabyte of address space (1159992K, specifically). As you might suspect, the 'page alloc' pages are carved out of an overall 'page summary' block of memory, and this and related things are found in runtime/mpagealloc.go and runtime/mpagealloc_64bit.go . The 'scavenge index' is from runtime/mgcscavenge.go , although the name is assigned to the mapping in runtime/mpagealloc_64bit.go (page allocation and scavenging are connected to each other). The 'heap index' is from runtime/malloc.go ; everything in this category (of which there are probably multiple allocations merged together) is a 'L2 arena map'.

This is a lot more reserved address space than Go 1.11 used back in 2018 , but we can also see that only about 36572 KBytes of it is actually allocated as accessible memory (with read and write permissions), which is not much more than Go 1.11 used. This difference between simply fencing off some address space and actually allocating memory into it can matter a lot in some circumstances .

(Not shown is how much of that memory has actually been touched and is either resident or dirty. Programs often establish read/write memory mappings without touching every page of them.)

Next is the special kernel ' vdso ' ( also ) support, which the kernel maps into everyone's address space, in a block from 0x7b3b5812e000 to 0x7b3b58136000:

7b3b5812e000       16K r--p  [vvar]
7b3b58132000        8K r--p  [vvar_vclock]
7b3b58134000        8K r-xp  [vdso]

Then the official process stack, which is initially allocated by the kernel when it exec()s the process, before Go is involved:

7ffc60b8f000      136K rw-p  [stack]

And then we have a special ' vsyscall ' area ( which is apparently a legacy thing ):

ffffffffff600000    4K --xp  [vsyscall]

On suitable Linux systems, Go sets the names of all of these areas using a function in runtime/set_vma_name_linux.go under the right circumstances. In general, this is controlled by the decoratemappings option for $GODEBUG , which was added in Go 1.25 and defaults to being on in that version and later. However, simply building your program with Go 1.25 or later isn't sufficient, as I found out; if your go.mod names an earlier Go version, Go will default to decoratemappings=0 even if you build your Go program with a later version.

In addition to building your program with a modern Go version and either having Go 1.25 or later set in go.mod or explicitly setting GODEBUG=decoratemappings=1 when you run your program, you need to be using a Linux kernel with this feature enabled. This is controlled by the kernel configuration option CONFIG_ANON_VMA_NAME and different distributions set it differently. Ubuntu has turned it on since at least 22.04 LTS (the oldest version I have access to right now), while Fedora has it off.

(Apparently the reason to leave it off is that it prevents merging adjacent virtual memory areas with the same permissions but that have different names. For example, the first two memory areas at 0x7b3b11460000 seem to get collapsed into one with 'decoratemappings=0'.)