分析 Linux 上 Go 1.27 程序的内存映射
文章背景与核心概要
本文基于 Ubuntu 26.04 LTS(64位 x86 架构)与 Go 1.27rc2 环境,深入探讨了一个极简 Go 程序的 Linux 操作系统内存使用情况。得益于 Go 1.25 中引入的 decoratemappings 特性,开发者现在可以通过查看 /proc/<pid>/maps 文件清晰识别和分类 Go 进程的虚拟内存空间,包括机器代码段、Go 随机化堆、内部元数据、页面分配器以及内核映射区。
概要
Summary
本文探讨了运行在 Go 1.27rc2(64位 x86 Ubuntu 26.04 LTS 服务器)上的极简 Go 程序的 Linux 操作系统内存使用情况。利用现代 Go 运行时特性——特别是在 Go 1.25 中引入的 decoratemappings 功能——我们可以查看 /proc/<pid>/maps,清晰地对虚拟内存分配进行分类,范围涵盖机器代码、随机化 Go 堆、内部元数据、页面分配器以及内核映射。
This article explores the Linux OS memory usage of a minimalist Go program running on Go 1.27rc2 (on a 64-bit x86 Ubuntu 26.04 LTS server). Utilizing modern Go runtime features—specifically the
decoratemappingsfeature introduced in Go 1.25—we can inspect/proc/<pid>/mapsto clearly categorize virtual memory allocations, ranging from machine code and the randomized Go heap to internal metadata, page allocators, and kernel mappings.
1. 引言
1. Introduction
多年前,有人曾发表过关于 简单 Go 1.11 程序的 Linux 操作系统内存使用情况 的深入分析。在 Go 1.27rc2 中重新探讨这一主题时,Go 现在能够向我们展示关于其内存使用的更多信息。
Many years ago, a deep dive was published regarding the Linux OS memory use of a simple Go 1.11 program. Returning to this topic in Go 1.27rc2, Go can now tell us much more information about its memory usage.
使用下面这个非常简单的程序:
Using this very simple program:
package main
func main() {
var i uint64
for {
i++
}
}
请确保在 go.mod 文件中设置了 go 1.25 或更高版本。在合适的 Linux 发行版上运行此程序会生成一个信息丰富的 /proc/<pid>/maps 文件,可以将其按区域划分并重新格式化,以显示具体大小而非起止地址范围。
Ensure you set
go 1.25or higher in yourgo.modfile. Running this on a suitable Linux distribution yields an informative/proc/<pid>/mapsfile, which can be broken into sections and reformatted to show sizes instead of start–end ranges.
2. 程序代码与静态数据
2. Program Code and Static Data
首先,我们检查程序的机器代码、只读数据、已初始化的变量以及零初始化数据 (bss) 空间:
First, we examine the program's machine code, read-only data, initialized variables, and zeroed data (
bss) space:
00400000 504K r-xp memdemo
0047e000 656K r--p memdemo
00522000 32K rw-p memdemo
0052a000 212K rw-p
该区块是连续的,覆盖从 0x00400000 到 0x0055f000 的范围。
* r, w, x:读取、写入和代码执行权限。
* p:写时复制(私有)映射。
This block is contiguous, covering
0x00400000to0x0055f000. *r,w,x: Read, write, and code execution permissions. *p: Copy-on-write (private) mapping.
(与 2018 年的 Go 1.11 版本 相比,程序本身变大了,这可能是由于各种隐式拉入的运行时包有所扩展。)
(Comparing this to the Go 1.11 version from 2018, the program itself has grown, likely due to expansions in various implicitly pulled-in runtime packages.)
3. Go 堆内存
3. The Go Heap
接下来是主要的 Go 堆及其初始 64 MByte arena。该程序实际上只需要其中的 4 MByte 块进行实际分配,其余部分保持为保留空间:
Next is the main Go heap with its initial 64 MByte arena. This program only required a 4 MByte chunk out of it for actual allocation, keeping the rest as reserved space:
d75e4000000 12288K ---p [anon: Go: heap reservation]
d75e4c00000 4096K rw-p [anon: Go: heap]
d75e5000000 49152K ---p [anon: Go: heap reservation]
该连续区块从 0xd75e4000000 延伸至 0xd75e8000000。
This contiguous block runs from
0xd75e4000000through0xd75e8000000.
Go 堆的位置过去是固定的,但自 Go 1.26 以来,在 64 位平台上随机化堆基地址已成为默认行为。这仍然是一个持续中的 Go 实验特性(来源),在 internal/goexperiment 中被追踪为 RandomizedHeapBase64。
The Go heap's location used to be fixed, but since Go 1.26, randomizing the heap base address is the default on 64-bit platforms. This remains a lingering Go experiment (via) tracked as
RandomizedHeapBase64in internal/goexperiment.
4. Go 内部内存分配
4. Go Internal Allocations
接下来是 Go 内部分配的各种组合,连续堆叠在一起:
Next are an assortment of Go internal allocations, stacked consecutively:
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]
该区域从 0x7b3b11460000 延伸至 0x7b3b5812e000,包含略多于 1 GB 的地址空间(约 1,159,992 KB),不过最大的部分是无法访问的(带有 ---p 标记,没有任何读、写或执行权限)。
This region runs from
0x7b3b11460000through0x7b3b5812e000, encompassing just over 1 GB of address space (~1,159,992 KB), though the largest portions are inaccessible (---pwith no read, write, or execution permissions).
- Page alloc/summary(页面分配器/摘要): 定义于
runtime/mpagealloc.go和runtime/mpagealloc_64bit.go。 - Scavenge index(清除索引): 来自
runtime/mgcscavenge.go。 - Heap index(堆索引): 来源于
runtime/malloc.go的 L2 arena map。
- Page alloc/summary: Defined in
runtime/mpagealloc.goandruntime/mpagealloc_64bit.go.- Scavenge index: From
runtime/mgcscavenge.go.- Heap index: An L2 arena map sourced from
runtime/malloc.go.
虽然这比 2018 年的 Go 1.11 保留了多得多的地址空间,但实际分配的可访问内存仅约 36,572 KB。在 某些场景下,保留地址空间与物理内存分配之间的这种区别至关重要。
While this reserves significantly more address space than Go 1.11 in 2018, only about 36,572 KB is actually allocated as accessible memory. This distinction between reserved address space and physical memory allocation is critical in certain scenarios.
5. 内核、栈与传统映射
5. Kernel, Stack, and Legacy Mappings
在 Go 特定的段之后,我们能找到系统层面的分配:
Following the Go-specific segments, we find system-level allocations:
VDSO 支持
VDSO Support
由内核映射到每个地址空间中(从 0x7b3b5812e000 到 0x7b3b58136000):
Mapped by the kernel into every address space (from
0x7b3b5812e000to0x7b3b58136000):
7b3b5812e000 16K r--p [vvar]
7b3b58132000 8K r--p [vvar_vclock]
7b3b58134000 8K r-xp [vdso]
(参见 vdso(7) 和 Kernel Internals VDSO)
(See vdso(7) and Kernel Internals VDSO)
进程栈
Process Stack
在 Go 运行时初始化之前,由内核在 exec() 运行进程时分配:
Allocated by the kernel when
exec()runs the process, prior to Go runtime initialization:
7ffc60b8f000 136K rw-p [stack]
Vsyscall 区域
Vsyscall Area
一种传统的机制(LWN 文章,Kernel Internals):
A legacy mechanism (LWN Article, Kernel Internals):
ffffffffff600000 4K --xp [vsyscall]
6. 启用映射装饰器
6. Enabling Mapping Decorators
在支持的 Linux 系统上,Go 通过 runtime/set_vma_name_linux.go 命名这些内存区域。此行为由 $GODEBUG 中的 decoratemappings 选项控制,该选项在 Go 1.25 及更高版本中默认启用。
On supported Linux systems, Go names these memory regions via
runtime/set_vma_name_linux.go. This behavior is controlled by thedecoratemappingsoption in$GODEBUG, which defaults to enabled in Go 1.25 and later.
映射名称的要求:
Requirements for Mapping Names:
- Go 版本: 使用 Go 1.25 或更高版本构建,且在
go.mod中指定了go 1.25(或更高)。如果指定了旧版本,Go 默认会使用decoratemappings=0。(你也可以在运行时显式传递GODEBUG=decoratemappings=1)。
- Go Version: Built with Go 1.25 or later, and
go 1.25(or higher) specified ingo.mod. If an older version is specified, Go defaults todecoratemappings=0. (You can also explicitly passGODEBUG=decoratemappings=1at runtime).
- Linux 内核支持: 内核必须启用了
CONFIG_ANON_VMA_NAME配置选项。 - Ubuntu: 至少自 22.04 LTS 起已默认启用。
- Fedora: 默认禁用。
- Linux Kernel Support: The kernel must have the
CONFIG_ANON_VMA_NAMEconfiguration option enabled.- Ubuntu: Enabled since at least 22.04 LTS.
- Fedora: Disabled by default.
(注意:禁用映射名称允许内核将共享权限但具有不同名称的相邻虚拟内存区域进行合并——例如,将 0x7b3b11460000 处的前两个内存区域合并为一个单区块)。
(Note: Disabling mapping names allows the kernel to merge adjacent virtual memory areas that share permissions but have different names—for instance, collapsing the first two memory regions at
0x7b3b11460000into a single block).