Golang链接共享库导致内存占用增加的问题

Golang链接共享库导致内存占用增加的问题 系统信息 Linux host1 4.4.0-131-generic #157-Ubuntu SMP 2018年7月12日 星期四 15:51:36 UTC x86_64 x86_64 x86_64 GNU/Linux Go版本 go version go1.10.4 linux/amd64

我有一个如下的Hello World应用程序:

package main
import "fmt"
func main() {
    fmt.Println("hello world")
    for {
    }
}

当我使用-linkshared标志编译时,看到RES内存变得非常高,约20MB 然而,当我静态构建不使用linkshared标志时,RES内存占用只有约1MB

这是怎么回事?我没想到使用共享库(linkshared)时RES内存会增加。应该只有共享内存会增加才对…

我启动了3个应用程序实例。

静态链接构建(1.4MB内存占用)

go build hello-world.go

$ top -b -n1 | grep hello
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
59838 root 20   0    3164   1428   1020 R 106.7  0.0   0:12.26 hello-world
59847 root  20   0    3164   1428   1020 R 106.7  0.0   0:10.05 hello-world
59853 root  20   0    3164   1480   1084 R 100.0  0.0   0:08.63 hello-world

$ ldd hello-world
not a dynamic executable

共享库构建(20MB内存占用)

go build -linkshared hello-world.go

$ top -b -n1 | grep hello
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
61395 root  20   0  158820  20396  12684 R 106.2  0.2   0:12.85 hello-world
61405 root  20   0  158820  20484  12760 R 106.2  0.2   0:11.38 hello-world
61385 root  20   0  158820  20464  12744 R 100.0  0.2   0:15.11 hello-world

$ ldd hello-world
linux-vdso.so.1 =>  (0x00007ffff6ed1000)
libstd.so => /usr/lib/go-1.10/pkg/linux_amd64_dynlink/libstd.so (0x00007f40e9d8a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f40e99c0000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f40e97bc000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f40e959f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f40ec218000)

更多关于Golang链接共享库导致内存占用增加的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

据我所知,该库的常驻部分(除了共享部分之外)也会被计入RSS。我敢肯定这些库还会分配各种资源,比如用于各类功能的表结构。

更多关于Golang链接共享库导致内存占用增加的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


使用-linkshared标志编译Go程序时内存占用增加是正常现象,这主要源于动态链接机制的内存管理方式。以下是具体原因分析:

内存占用差异的原因

  1. 共享库的加载机制:当使用动态链接时,整个Go运行时库(libstd.so)会被映射到每个进程的地址空间中,虽然这部分内存可以在进程间共享,但RES(常驻内存)统计仍会计算每个进程映射的完整大小。

  2. 内存页对齐开销:动态库通常按内存页边界对齐加载,即使只使用库中的少量功能,也会加载整个库的文本段和数据段。

  3. 全局数据段复制:共享库中的可写数据段(BSS、DATA)会在每个进程中有独立的副本,无法在进程间共享。

示例验证

可以通过以下代码验证内存分配情况:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func printMemStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v\n", m.NumGC)
}

func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

func main() {
    printMemStats()
    
    // 分配一些内存来观察变化
    data := make([]byte, 1024*1024) // 1MB
    for i := range data {
        data[i] = 1
    }
    
    printMemStats()
    
    // 保持程序运行以便观察top输出
    for {
        time.Sleep(time.Second)
    }
}

技术细节

使用-linkshared时,Go编译器会:

  • 将Go运行时、标准库等编译为共享库libstd.so
  • 生成依赖该共享库的可执行文件
  • 每个进程加载完整的libstd.so到内存中

静态链接则将所有需要的代码编译到单个可执行文件中,内存管理更加紧凑。

性能对比

可以通过以下命令查看具体的内存映射:

# 查看进程内存映射
cat /proc/<pid>/maps

# 查看更详细的内存信息
cat /proc/<pid>/smaps

这种内存占用差异是动态链接架构的固有特性,在需要运行多个Go进程实例且内存受限的环境中,静态链接通常是更好的选择。

回到顶部