接着我第一反应是去翻了 Go FAQ(因为看到过,有印象),其问题为 "Why does my Go process use so much virtual memory?",回答如下:
The Go memory allocator reserves a large region of virtual memory as an arena for allocations. This virtual memory is local to the specific Go process; the reservation does not deprive other processes of memory.
To find the amount of actual memory allocated to a Go process, use the Unix top command and consult the RES (Linux) or RSIZE (macOS) columns.
在此比较可疑的是 mmap 方法,它在 dtruss 的最终统计中一共调用了 10 余次,我们可以相信它在 Go Runtime 的时候进行了大量的虚拟内存申请。
我们再接着往下看,看看到底是在什么阶段进行了虚拟内存空间的申请。
注:若是 Linux 系统,可使用 strace 命令。
查看 Go Runtime
启动流程
通过上述的分析,我们可以知道在 Go 程序启动的时候 VSZ 就已经不低了,并且确定不是共享库等的原因,且程序在启动时系统调用确实存在 mmap 等方法的调用。
那么我们可以充分怀疑 Go 在初始化阶段就保留了该内存空间。那我们第一步要做的就是查看一下 Go 的引导启动流程,看看是在哪里申请的。
引导过程如下:
graph TD A(rt0_darwin_amd64.s:8<br/>_rt0_amd64_darwin) -->|JMP| B(asm_amd64.s:15<br/>_rt0_amd64) B --> |JMP|C(asm_amd64.s:87<br/>runtime-rt0_go) C --> D(runtime1.go:60<br/>runtime-args) D --> E(os_darwin.go:50<br/>runtime-osinit) E --> F(proc.go:472<br/>runtime-schedinit) F --> G(proc.go:3236<br/>runtime-newproc) G --> H(proc.go:1170<br/>runtime-mstart) H --> I(在新创建的 p 和 m 上运行 runtime-main)
runtime-osinit:获取 CPU 核心数。
runtime-schedinit:初始化程序运行环境(包括栈、内存分配器、垃圾回收、P等)。
runtime-newproc:创建一个新的 G 和 绑定 runtime.main。
runtime-mstart:启动线程 M。
注:来自@曹大的 《Go 程序的启动流程》和@全成的 《Go 程序是怎样跑起来的》,推荐大家阅读。
初始化运行环境
显然,我们要研究的是 runtime 里的 schedinit 方法,如下:
func schedinit() { ... stackinit() mallocinit() mcommoninit(_g_.m) cpuinit() // must run before alginit alginit() // maps must not be used before this call modulesinit() // provides activeModules typelinksinit() // uses maps, activeModules itabsinit() // uses activeModules
msigsave(_g_.m) initSigmask = _g_.m.sigmask
goargs() goenvs() parsedebugvars() gcinit() ... }
从用途来看,非常明显, mallocinit 方法会进行内存分配器的初始化,我们继续往下看。
初始化内存分配器
mallocinit
接下来我们正式的分析一下 mallocinit 方法,在引导流程中, mallocinit 主要承担 Go 程序的内存分配器的初始化动作,而今天主要是针对虚拟内存地址这块进行拆解,如下:
func mallocinit() { ... if sys.PtrSize == 8 { for i := 0x7f; i >= 0; i-- { var p uintptr switch { case GOARCH == "arm64" && GOOS == "darwin": p = uintptr(i)<<40 | uintptrMask&(0x0013<<28) case GOARCH == "arm64": p = uintptr(i)<<40 | uintptrMask&(0x0040<<32) case GOOS == "aix": if i == 0 { continue } p = uintptr(i)<<40 | uintptrMask&(0xa0<<52) case raceenabled: ... default: p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32) } hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) hint.addr = p hint.next, mheap_.arenaHints = mheap_.arenaHints, hint } } else { ... } }
判断当前是 64 位还是 32 位的系统。
从 0x7fc000000000~0x1c000000000 开始设置保留地址。
判断当前 GOARCH、 GOOS 或是否开启了竞态检查,根据不同的情况申请不同大小的连续内存地址,而这里的 p 是即将要要申请的连续内存地址的开始地址。
for i := 0x7f; i >= 0; i-- { ... hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) hint.addr = p hint.next, mheap_.arenaHints = mheap_.arenaHints, hint }
var mheap_ mheap ... func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { ... for h.arenaHints != nil { hint := h.arenaHints p := hint.addr if hint.down { p -= n } if p+n < p { v = nil } elseif arenaIndex(p+n-1) >= 1<<arenaBits { v = nil } else { v = sysReserve(unsafe.Pointer(p), n) } ... }
你可以惊喜的发现 mheap.sysAlloc 里其实有调用 sysReserve 方法,而 sysReserve 方法又正正是从 OS 系统中保留内存的地址空间的特定方法,是不是很惊喜,一切似乎都串起来了。
Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。
Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。