0x01 最简单的Go程序

package main

import (
"fmt"
"time"
) func main() {
fmt.Println("hello")
for {
// sleep一下,避免CPU占用过高。
time.Sleep(1 * time.Second)
}
}

不含调试符号的二进制大小为1229464Byte,约1.2MiB。

编译出二进制,同样通过readelf工具检查下。看到Program Header中有3个可加载段。

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x0815e7 0x0815e7 R E 0x1000
LOAD 0x082000 0x0000000000482000 0x0000000000482000 0x091cd0 0x091cd0 R 0x1000
LOAD 0x114000 0x0000000000514000 0x0000000000514000 0x017ea0 0x0497f0 RW 0x1000

根据Flg可以判断分别对应代码、常量、变量。内存大小分别为:517.47KiB、583.20KiB、293.98KiB。

大约是1.2MiB。

Go的堆和通常意义的进程堆不是等同的。大小关系为

Go堆 < 进程堆

这个分析的目的是:

  1. 找出Go进程堆除了Go堆,还包含了哪些内容?
  2. 为什么看到的Go进程的RSS值,远大于Go的堆?

0x02 详细分析

再通过readelf -W -S main查看,可以确认变量区保存了以下section

类型段见文档elf中解释的。

PROGRBITS:指完全由elf文件中的内容定义。

NOBITS:不占用文件的实际空间,但有真实的偏移。告诉加载器,不用真的去读文件内容。

Section 大小(Bytes) 类型 说明
.go.buildinfo 224 PROGBITS Go的构建信息
.noptrdata 67104 PROGBITS 非指针类型数据,大约65KiB
.data 30608 PROGBITS 可能是指针类型的数据,大约29KiB
.bss 183KiB NOBITS 未初始化全局静态,猜测是给Go runtime使用C栈部分使用的。
.noptrbss 14KiB NOBITS

除了C语言已有段,Go还有自己的自定义Section如下。

都包含在了常量段中。

段名 解释 备注
.typelink 类型链接段 modules中记录在types类型表中的偏移,或者是typemap中的key
.itablink 接口链接段 大小跟接口数量有关
.gosymtab 符号段 strip后,这个段为空
.gopclntab Go中用于栈回溯用到的pc和栈帧映射表,每个module有自己的表 程序代码越多,此段越大。

go中的module并不是package。go支持plugin,所以一般编译出来的程序只有一个module。

readelf看到的可加载的Section有12个,但查看main的/proc/<pid>/smap可以看到多达22个Section。

查看smap中的段

用途 来源 权限 对应Section
00400000-00482000 代码段 二进制中的代码 r-xp
00482000-00514000 常量段 二进制中的 r--p
00514000-0052c000 变量段 二进制中 rw-p .go.buildinfo .noptrdata .bss 一段
0052c000-0055e000 变量段 二进制中 rw-p .bss一段和noptrbss
c000000000-c000400000 根据寄存器的内容看是栈 rw-p
c000400000-c004000000 也是栈。 ---p
7fcce0548000-7fcce2800000 mspan
7fcce2800000-7fcce2c00000 mspan rw-p
7fcce2c00000-7fcce2c04000 mspan rw-p
7fcce2c04000-7fccf317d000 mspan ---p
7fccf317d000-7fccf317e000 mspan rw-p
7fccf317e000-7fcd0502d000 mspan ---p
7fcd0502d000-7fcd0502e000 mspan rw-p
7fcd0502e000-7fcd07403000 mspan rw-p
7fcd07403000-7fcd07404000 mspan rw-p
7fcd07404000-7fcd0787d000 mspan ---p
7fcd0787d000-7fcd0787e000 mspan rw-p
7fcd0787e000-7fcd078fd000 mspan ---p
7fcd078fd000-7fcd0795d000 mspan rw-p
7ffdd88fd000-7ffdd891f000 C栈和system栈 rw-p
7ffdd898c000-7ffdd8990000 vvar r--p
7ffdd8990000-7ffdd8992000 vdso r-xp

注:

  1. ---p类型的是从背后看但还没映射物理页的区域。
  2. mspan为笔者加的,实际看smaps内容,没有这样的标识。笔者通过dlv等手段判定的。mspan是Go中管理Go堆的数据结构。

Q:为什么mspan和堆区隔了比较大的空间?

A:mspan是通过指定mmap flag为 mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0)分配的。而堆区有严格的地址空间约束,是指定起始地址通过mmap向操作系统主动占用的。这样做的好处是可以很方便通过地址判断属于哪个内存区域。

待分配的空间通过runtime.mheap_.arenaHints(单向链表)管理的。1.20之后增加了userArena。通过dlv调试,可以确认c000000000-c000400000栈段是通过runtime.sysAlloc分配的,即指定起始地址的mmap方式分配的。分配之后就会从runtime.mheap_.arenaHints中移除掉。

通过dlv中看到的内容如下:

(dlv) p %x runtime.mheap_.arenaHints
*runtime.arenaHint {
_: runtime/internal/sys.NotInHeap {
_: runtime/internal/sys.nih {},},
addr: c004000000,
down: %!x(bool=false),
next: *runtime.arenaHint {
_: (*"runtime/internal/sys.NotInHeap")(0x7fcd079219c8),
addr: 1c000000000,
down: %!x(bool=false),
next: *(*runtime.arenaHint)(0x7fcd079219b0),},}

§ 0x03 小结

通过以上分析得到的Go进程的内存RSS占用组成如下:

总体来说,对Go内存影响最大的两个因素:

  1. 业务越复杂,代码、常量、全局变量占用的就越多,特别是常量区的.gopclntab增长最为明显。以kubectl为例,42MiB的二进制,这个段占据了大概12MiB。
  2. 业务并发越多,协程越多,Go堆的占用就越多。因为Go堆包含了用户协程栈。

§ 0x04 为什么Go进程RSS高?

另外,因为用Go开发大多是一些容器应用,要求这些程序静态编译,结果就是Go的代码段不可能与其他Go应用复用,所以Go的RSS会较高。

还有,因为Go自己实现了栈管理,所以回溯栈(打堆栈、GC场景)时需要依赖pclntab,这部分信息如上所述,也占据很大内存空间,同样不同Go程序也不能复用,所以更加剧了Go的内存占用。

从上述两个因素出发,如何优化内存占用呢?

4.1 减小代码段

动态编译后面社区大概率是不会支持了,见issue。所以想通过这种方式减少Go的代码段,不可行。

Go相比Python这种脚本语言有个问题,一个package下的源文件,即便里面的某个接口你用不到,只要你用到了其中一个,其他的源文件就被编译进二进制中。

如果可以识别哪部分占比大,同时又不用的话,就可以这样本地化之后,通过//go:build的构建标签来优化。查看二进制中哪个package贡献大小,可以使用如下的工具查看: https://github.com/goccy/go-graphviz

4.2 GC调优

另外一个思路就是GC调优。

虽然GO只有一个GOGC的调优参数,但关联较多,需要额外一个文档才能说清楚。

§ 0x05 参考

  1. mmap与brk区别 https://www.cnblogs.com/vinozly/p/5489138.html
  2. Go二进制可视化 https://github.com/goccy/go-graphviz

Go进程内存占用那些事(二)的更多相关文章

  1. Linux系统下输出某进程内存占用信息的c程序实现

    在实际工作中有时需要程序打印出某个进程的内存占用情况以作参考, 下面介绍一种通过Linux下的伪文件系统/proc 计算某进程内存占用的程序实现方法. 首先, 为什么会有所谓的 伪文件 呢. Linu ...

  2. Linux查看进程内存占用及内存使用情况

    LINUX进程内存占用查看方法(1)top可以直接使用top命令后,查看%MEM的内容.可以选择按进程查看或者按用户查看,如想查看oracle用户的进程内存使用情况的话可以使用如下的命令:$ top ...

  3. 监控Linux系统所选的服务所占进程内存占用

    [代码] #!/bin/bash #程序功能描述: # 监控系统所选的服务所占进程内存占用 #作者:孤舟点点 #版本:1.0 #创建时间:-- :: PATH=/bin:/sbin:/usr/bin: ...

  4. window 实用操作(结束已打开无法删除进程 内存占用)

    1.win7删除文件,文件夹或文件已在另一程序中打开:https://jingyan.baidu.com/article/e75057f2a41e88ebc91a8985.html 删除文件时,提示“ ...

  5. 查看LINUX进程内存占用情况

    可以直接使用top命令后,查看%MEM的内容.可以选择按进程查看或者按用户查看,如想查看oracle用户的进程内存使用情况的话可以使用如下的命令: (1)top top命令是Linux下常用的性能分析 ...

  6. 查看LINUX进程内存占用情况(转)

    可以直接使用top命令后,查看%MEM的内容.可以选择按进程查看或者按用户查看,如想查看oracle用户的进程内存使用情况的话可以使用如下的命令: (1)top top命令是Linux下常用的性能分析 ...

  7. 查看LINUX进程内存占用情况及启动时间

    可以直接使用top命令后,查看%MEM的内容.可以选择按进程查看或者按用户查看,如想查看oracle用户的进程内存使用情况的话可以使用如下的命令: (1) top top命令是Linux下常用的性能分 ...

  8. linux 查看某个进程内存占用情况命令

    1.先用ps查询进程号 ps -aux|grep 进程名字 2.查看更详细的内存占比 cat /proc/3664/status 返回结果:(其中VmRSS为进程所占用的内存)

  9. linux查看进程内存占用

    ps -aux | grep xxx USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND 可以看到RSS ...

  10. 使用showmap分析android进程内存占用情况(转载)

    转自:http://my.oschina.net/shaorongjie/blog/105354 可以使用adb shell showmap pid查看一个进程的showmap,这对于我们来说非常有用 ...

随机推荐

  1. 用pm2命令管理你的node项目

    文章目录 前言 安装 运行项目 pm2的命令 前言 我在服务器上运行node项目,使用命令nohup npm start &,结果关闭终端之后,进程就会停止,看来nohup也不是万能的后台运行 ...

  2. Spark Structured Streaming(一)基础

    1. 流处理的场景 我们在定义流处理时,会认为它处理的是对无止境的数据集的增量处理.不过对于这个定义来说,很难去与一些实际场景关联起来.在我们讨论流处理的优点与缺点时,先介绍一下流处理的常用场景. 通 ...

  3. Linux 使用 Swap分区

    Linux 使用 Swap分区 背景 买的云服务器在使用的时候,资源经常不够,因此需要使用swap分区. Swap分区在系统的物理内存不够用的时候,把硬盘内存中的一部分空间释放出来,以供当前运行的程序 ...

  4. Android 各层架构

    Android应用框架层和硬件抽象层以及底层之间的关系 1. JNI技术: (1).JNI技术简单的说就是在本地Java语言声明本地方法和加载动态链接库(.so文件) (2).动态链接库(.so文件) ...

  5. 阿里云日志Nginx日志分析

    每分钟接口访问次数的前200条统计 not request_uri : "/heartbeat.html" | SELECT time_series(time, '1m', '%H ...

  6. Excel 更改数据同步更新到Mysql数据库

    刚上班,领导给我提出一个需求,想要每天更新Mysql数据库中的原有商品订单状态,添加新的商品订单状态.因为公司目前的数据库只能添加数据,不能更改数据,想要更改原有的数据,只能将原有的数据清空,再导入新 ...

  7. VulnHub-DC-7渗透流程

    DC-7 kali:192.168.157.131 靶机:192.168.157.151 信息收集 nmap -sV -A -p- 192.168.157.151 虽然有robots.txt等敏感文件 ...

  8. Java常见的加密方式

    前言 传说在古罗马时代,发生了一次大战.正当敌方部队向罗马城推进时,古罗马皇帝凯撒向前线司令官发出了一封密信:VWRS WUDIILF.这封密信被敌方情报人员翻遍英文字典,也查不出这两个词的意思. 此 ...

  9. 在Winform程序中增加隐藏的按键处理,用于处理一些特殊的界面显示或者系统初始化操作

    以前,我看到一个朋友在对一个系统做初始化的时候,通过一组魔幻般的按键,调出来一个隐藏的系统设置界面,这个界面在常规的菜单或者工具栏是看不到的,因为它是一个后台设置的关键界面,不公开,同时避免常规用户的 ...

  10. Jenkins插件管理(Manager Plugins)【快速提升项目构建和部署实施的工作效率】

    Jenkins 是一个很棒的开源自动化平台.它有一些开箱即用的强大功能.然而,在我看来,让它脱颖而出的是它的社区和它开发的插件.有超过一千个插件可用于支持几乎所有用于构建.部署和自动化项目的技术.工具 ...