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'.)