golang面经——内存相关模块 - 详解

news/2025/11/6 21:36:47/文章来源:https://www.cnblogs.com/gccbuaa/p/19197793

golang的内存布局

+----------------------------------------+  ← 高地址(如 0x7fff_ffff_ffff)
|                栈 (Stack)              |
|  - 每个 goroutine 有自己的栈           |
|  - 向下增长(向低地址方向扩展)         |
|  - 存放局部变量(不逃逸的)             |
+----------------------------------------+
|            命令行参数 & 环境变量        |
|  - argc, argv, envp                    |
+----------------------------------------+
|                                        |
|            空洞(未映射区域)           |
|      (保护栈和堆不碰撞,留空)         |
|                                        |
+----------------------------------------+
|           内存映射区 (Memory Mapping)  |
|  - Go runtime 通过 mmap 申请的大块内存 |
|    (如 mheap 的 arena 区域)          |
|  - cgo 分配的内存                      |
|  - os.Mmap 映射的文件                  |
|  - 动态链接库(若使用 cgo)            |
+----------------------------------------+
|                堆 (Heap)               |
|  - Go 的主要动态分配区域               |
|  - 向上增长(向高地址方向扩展)         |
|  - 由 mcache/mcentral/mheap 管理       |
|  - 存放逃逸对象、slice 底层数组、map 等 |
+----------------------------------------+
|          未初始化数据段 (BSS)          |
|  - 未显式初始化的全局/静态变量         |
|    (如 var x int; var s []string)    |
+----------------------------------------+
|          已初始化数据段 (Data)         |
|  - 显式初始化的全局/静态变量           |
|    (如 var count = 42)               |
+----------------------------------------+
|          只读数据段 (ROData)           |
|  - 字符串字面量:"hello"               |
|  - const 常量(部分)                  |
|  - 只读,防止修改                      |
+----------------------------------------+
|              代码段 (Text)             |
|  - 编译后的机器指令(函数代码)         |
|  - 只读、可共享                        |
+----------------------------------------+  ← 低地址(如 0x0000_0000_0040_0000)

1.谈谈内存泄露,什么情况下内存会泄露?怎么定位排查内存泄漏问题?

分析:

        首先要明白什么是内存泄漏?从字面意思很好理解,就是程序中存在内存不能及时被有效释放,导致这部分内存不可用,随着越来越多的可能出现内存泄漏,会出现内存用满,程序崩溃的情况。理解了内存泄漏之后,就可以思考一下go语言编码中哪些情况会出现这种情况?其实最常见的就是goroutine的阻塞不能快速释放,导致这部分内存一直占用着,随着goroutine越来越多,就内存泄漏了。

什么是内存泄漏

        内存泄漏就是程序生命周期中一些对象不能被及时回收,一直占用着内存,导致这部分内存不可用的情况。

go语言内存泄露原因:

        Go 语言中,虽然有垃圾回收器(GC)自动管理堆内存,但内存泄漏(Memory Leak)仍然可能发生。这是因为 GC 只回收不可达对象(unreachable objects),而如果程序意外地长期持有对不再需要对象的引用,这些对象就无法被回收,导致内存持续增长。

 常见的内存泄露的场景有:

1)goroutine泄露(最常见场景)

Goroutine 泄漏的本质

启动的 goroutine 无法正常退出,持续阻塞或运行,导致资源无法释放。

1、在 goroutine 中对 nil channel 读写 → goroutine 泄漏

举例:

func leak1() {var ch chan int // nil channelgo func() {ch <- 42 // 永久阻塞!因为 nil channel 的 send 永远不会成功}()time.Sleep(100 * time.Millisecond)fmt.Println("main done, but goroutine leaked")
}

2、只对满缓冲或者无缓冲 channel 发送不收;或者 处于接收阻塞的通道不关闭或不发送

举例:

func leak2() {ch := make(chan int) // 无缓冲go func() {ch <- 1 // 阻塞,因为没人收ch <- 2 // 永远不会执行到这里}()// 主 goroutine 不接收 → 子 goroutine 永久阻塞 → 泄漏time.Sleep(100 * time.Millisecond)
}
func leak3() {ch := make(chan int)go func() {<-ch // 阻塞,因为没人发}()// 主 goroutine 不发送也不 close → 子 goroutine 永久阻塞time.Sleep(100 * time.Millisecond)
}

3、Go 的 net/http 内部为每个连接启动读 goroutine,如果 Body 未读完或未关闭,该 goroutine 可能无法释放。

func leak4() {for i := 0; i < 1000; i++ {go func() {resp, _ := http.Get("https://httpbin.org/delay/1")// 忘记 resp.Body.Close()// resp.Body 未读完,连接无法复用,底层 goroutine 可能挂起}()}time.Sleep(5 * time.Second)
}
func fix4() {resp, err := http.Get("https://httpbin.org/delay/1")if err != nil {return}defer resp.Body.Close() // ✅ 必须关闭io.ReadAll(resp.Body)   // 可选:读完 body(某些服务要求)
}

4、死锁的情况也会导致阻塞

5、sync.WaitGroup 使用不当,Add 和 Done 不匹配

func leak6() {var wg sync.WaitGroupwg.Add(2) // 声明 2 个任务go func() {fmt.Println("task 1")wg.Done()}()// 忘记启动第二个 goroutine → wg.Wait() 永远阻塞wg.Wait() // 主 goroutine 阻塞,但即使主退出,第一个 goroutine 已退出,不算泄漏?
}

6、time.Ticker 必须调用 Stop,否则会一直占用内存。

  time.Ticker 内部启动一个 goroutine 定时向 channel 发送时间。如果不 Stop(),该 goroutine 永远不会退出,即使 ticker 变量已不可达(因为内部 goroutine 持有引用)

func leak7() {ticker := time.NewTicker(100 * time.Millisecond)go func() {for range ticker.C {fmt.Println("tick")break // 只执行一次}// 忘记 ticker.Stop()}()time.Sleep(200 * time.Millisecond)// ticker 的内部 goroutine 仍在运行!
}
func fix7() {ticker := time.NewTicker(100 * time.Millisecond)defer ticker.Stop() // ✅ 或在 goroutine 中 Stopgo func() {defer ticker.Stop()for range ticker.C {fmt.Println("tick")break}}()time.Sleep(200 * time.Millisecond)
}

2)未关闭的资源

        忘记关闭 *os.Filenet.Connhttp.Response.Bodysql.Rows 等,这些资源内部可能持有内存、文件描述符、网络连接等。

3)全局变量或长生命周期容器持续增长

        全局 map、slice、cache 不断添加元素,从不清理;即使元素不再使用,因被全局变量引用,GC 无法回收。

var cache = make(map[string]*Data)
func add(key string, data *Data) {cache[key] = data // 持续增长,无过期机制
}

如何排查

分析:

对于go语言的性能分析,比如cpu,内存一般会使用pprof工具来进行分析。

回答:

        单个函数:调用 runtime.NumGoroutine方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。

        生产/测试环境:使用 pprof 实时监测Goroutine的数量。

2.知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?什么是内存逃逸?

分析:

内存逃逸(Memory Escape)是指本应在栈上分配的变量,由于其生命周期超出了当前函数的作用域,被 Go 编译器自动分配到堆上的现象。这是 Go 编译器在逃逸分析(Escape Analysis)阶段做出的优化决策,目的是确保程序在运行时内存安全。.

对象分配在栈上还是堆上,由编译器在编译期通过“逃逸分析”(Escape Analysis)自动决定。开发者无法显式控制,但可以通过理解规则来预测和优化。

        对象大小是否对内存逃逸有影响?

        在 Go 语言中,对象是否发生内存逃逸,并不直接由其大小决定,而是由其“生命周期是否超出当前函数栈帧”决定。也就是说,逃逸分析的核心是“作用域和引用关系”,而不是大小

        函数内直接申请32K以上的对象,会出现内存逃逸,会直接向mheap申请内存。

        小对象也可以通过返回指针;channel发送指针等方式进行内存逃逸。详细场景下面介绍

具体的逃逸分析策略有以下几种:

        1.如果函数内的变量外部没有引用,则优先放到栈中,

        2.如果函数外部的变量存在引用,则必定放到堆中;

        3.如果栈上申请大于32KB,则必定放到堆上;

        前两种主要考虑对象的生命周期,当这个对象的生命周期不能被确定,不能跟随当前函数结束而结束,就会发生逃逸,被分配到堆上。

回答:

        内存逃逸是编译器在程序编译时期根据逃逸分析策略,将原本应该分配到栈上的对象分配到堆上的一个过程。

以下场景会发生内存逃逸:

1.方法内返回局部变量指针。

func createInt() *int {x := 42return &x  // x 逃逸到堆
}

2.向 channel 发送指针数据。

ch := make(chan *int)
func send() {x := 10ch <- &x  // x 逃逸(因为 channel 可能被其他 goroutine 持有)
}

3.在闭包中引用包外的值。

func counter() func() int {n := 0return func() int {n++return n} // n 逃逸到堆,因为闭包可能在函数返回后调用
}

4.切片(扩容后)长度太大。

func largeSlice() []byte {s := make([]byte, 1024*1024) // 大对象可能直接分配到堆return s
}

5.在 slice 或 map 中存储指针。

// main.go
package main
func main() {var s []*intfor i := 0; i < 3; i++ {x := i // 局部变量s = append(s, &x) // 取地址并存入 slice}_ = s
}

6.在 interface 类型上调用方法。

func main() {x := 100var i interface{} = x // 赋值给 interface{}_ = i
}

查看是否出现内存逃逸的方法

go build -gcflags="-m -l" main.go
  • -m:打印逃逸分析和内联决策;
  • -l:禁止内联(避免干扰分析)。
// main.go
func f() *int {x := 1return &x
}
go build -gcflags="-m -l" main.go
./main.go:2:9: &x escapes to heap
./main.go:1:14: moved to heap: x

内存逃逸有什么影响

分析:

       这个问题可以从栈对象和堆对象的区别,堆对象需要垃圾回收机制来释放内存,栈对象会跟随函数结束被编译器回收。

回答:

        但 Go 的逃逸分析非常成熟,开发者通常无需手动干预,只需理解其原理,避免不必要的指针返回或大对象逃逸。

3.请简述 Go 是如何分配内存的?

分析:

        Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池, + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、mcentral、mcache。它们以 mspan 作为基本分配单位。

回答:

        go语言对象的分配根据对象大小的不同申请策略也不同:

        当要分配大于 32K 的对象时,从 mheap 分配。

        当要分配的对象小于等于 32K 大于 16B 时,从P上的 mcache 分配,如果 mcache 没有内存,则从 mcentral获取,如果 mcentra 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。

        当要分配的对象小于等于 16B 时(微小对象),从 mcache 上的微型分配器上分配。

4.Channel分配在栈上还是堆上?哪些对象分配在堆上,哪些对象分配在堆上?

channel分配在栈上还是堆上?

分析:

        这个题可以看作是对上一个题理解程度的考察,可以用内存逃逸的思想来分析这个题,因为channel的作用就是用做两个goroutine之间的通信,所以在很大程度上它的生命周期并不会局限在一个函数内部,所以大概率会发生内存逃逸,就很容易得出结论,channel大概率会分配到堆上。

回答:

        channel分配在堆上,Channel 被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以 golang 直接将其分配在堆上。

哪些对象分配在堆上,那些对象分配在栈上?

分析:

        对象是分配在栈上还是堆上跟go语言的语法没有关系,需要在编译期由编译器进行逃逸分析而决定。根据逃逸分析策略来思考。

回答:

        一般而言,大的对象直接分配在堆上,如果一个局部变量会被外部引用,生命周期不确定,也会分配到堆上。其他小对象会优先分配在栈上。

如果使用new申请一个类型的地址,其分配位置(堆 or 栈)不是由 new 决定的,而是由编译器的逃逸分析。

  • 如果不逃逸 → 分配在 栈上
  • 如果逃逸 → 分配在 堆上

5.介绍一下大对象小对象,什么情况下会导致GC压力大?

分析:

        首先GC压力大指的是我们GC的时候,需要占用比较高的CPU 时间和内存带宽资源,这就会影响我们用户Goroutine的执行。

        而当大量小对象 逃逸到堆上,就意味着这些小对象是需要被GC回收的(可能是在某一次GC),因为栈上的对象 可以 随着栈帧的释放而回收,而堆上的对象 只能由GC来进行内存管理。同样巨大的map和slice也就意味着GC需要处理更多的数据。

回答:

        go语言中小于等于 32k 的对象就是小对象,其中小于16B 的是微小对象(而且是不含指针的对象),其它都是大对象。

        当有大量小对象逃逸到堆上,或者有巨大的元素类型为指针的map和slice的情况下,GC压力会较大。

Go代码性能优化

1.你知道Go的哪些性能优化手段?

回答:

Go内存分配优化:核心就是内存逃逸和对象池。

        内存逃逸优化:属于那种听上去逼格很高,但是实际上效果非常有限的,将SOL优化一下,几十几百毫秒省出来了,但是Go内存分配优化来优化,可能也就优化了1毫秒。

一般是使用 go build -gcflags '-m'命令来看哪里发生了逃逸,然后"尝试优化掉"

        对象池:可以使用Go官方提供的sync.Pool,一般来说,如果你有什么接口是要处理比较大批量数据的,就可以考虑这种方案。

        并发优化:主要思路就是 有锁改无锁;写锁改读写锁;原子操作(CAS也可以看是乐观锁);并发优化这个在业务开发里面比较少用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/958125.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11/7

今天上午体育课比赛但没轮到我们组,说实话我感觉我们组如果上了应该可以拿个好名次,看完他们比赛

QOJ4795 Taxi

题意简述 给定一颗 \(n\) 个点的树,边有边权。有 \(m\) 个独立的乘客和 \(m\) 个独立的司机,每个人选一个节点。将乘客与司机匹配,使得距离之和最大。 求所有 \(n^{2m}\) 种可能情况的距离之和 \(\bmod 10^9+7\)。 …

蓝牙耳机怎么连接电脑?【图文详解】蓝牙耳机连接电脑?蓝牙耳机能连接电脑吗?USB蓝牙适配器? - 详解

蓝牙耳机怎么连接电脑?【图文详解】蓝牙耳机连接电脑?蓝牙耳机能连接电脑吗?USB蓝牙适配器? - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: bl…

AI浪潮下的就业迷思:技术迭代还是泡沫破灭?

最近刷到一堆AI相关的新闻,从OpenAI和亚马逊签下天价算力订单,到谷歌推出能预警洪水的地球级AI,再到微软自研图像模型减少对OpenAI的依赖,感觉整个科技圈都在疯狂押注AI。但作为一个学软件的学生,我反而有点焦虑:…

洛谷 P4159

给定一个 \(n\) 个节点的有向图,连接 \((i, j)\) 的有向边边权为 \(c_{i, j}(0 为没有边)\),问有多少种从 \(1\) 到 \(n\) 的方式使得经过的边边权之和为 \(k\)。 \(n \le 10, c \le 9, k \le 10^9\)。如果 \(c\) 只…

25.11.6 DAG和拓扑排序

一.DAG即有向无环图,常用于: 任务依赖:某任务必须在另一个任务完成后执行(如编译依赖、任务调度)。 课程顺序:先修课关系。 表达式计算顺序。 动态规划优化:例如在 DAG 上进行最长路径、最短路径 DP。 二.拓扑排…

2025-11-06 PQ v.Next日志记录

项目核心信息目前初步预计在这里进行开发测试(后续到develop): https://z.gitee.cn/zgca/projects/777586/repos/zgca/aipq/tree/feature%2Fteacher_feel今日进度(4*4): 当前任务:全员微信开发能力培训完成,活…

数据库介绍,安装,配置

https://www.cnblogs.com/linhaifeng/articles/7126847.html 1.数据库服务器:运行数据库管理软件的计算机 2.数据库管理软件:MySQL,Oracle,db2,SQLserver 3.库:文件夹 4.表:文件 5.记录:事物一系列的特征:cy,…

Spring BeanFactory 接口

[[Spring IOC 源码学习总笔记]] BeanFactory 的子接口 Spring BeanFactory 的设计, 基于接口隔离原则(Interface Segregation Principle), 将具有不同细分的功能定义为接口, 增加扩展性, 支持不同功能的 BeanFactory…

领码方案|微服务与SOA的世纪对话(3):方法论新生——DDD、服务网格与AI Ops的融合之道 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

遗留系统微服务改造(四):从单体到微服务的演进之路 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

备考笔记8

创建型:单抽原建厂 结构型:外桥组元代配饰 行为型:观模迭策责令解访介忘态 分页存储,位示图,多级索引,树的性质

不用Docker也能跑RustFS?Windows一键安装实测来了!

不用Docker也能跑RustFS?Windows一键安装实测来了!2025年,当所有人都在用Docker部署RustFS时,我们偏要反其道而行——​在Windows上直接运行RustFS二进制版​,实测结果令人惊喜:​​安装5分钟搞定,内存占用<…

Spacy 词性 实体 依存关系等对应缩写

dep: 依存关系标签(Dependency Label) ROOT -- None 中心词,通常是动词 最重要的词,从它开始,根节点acl -- clausal modifier of noun (adjectival clause)形容词性从句acomp -- adjectival complement 形容…

洛谷 P2824

显然直接排序是不可能的。这种关于排序的题目有个经典套路:先考虑只有 \(0, 1\) 的情况。 这个题就是用线段树维护区间 \(0/1\) 的数量,再区间赋 \(0/1\) 即可。 再考虑 \(n\) 个数,可以二分答案 \(x\),将 \(< …

JavaSE——基础

JavaSE——基础JavaSE——基础1.注释单行注释:// 多行注释:/* 注释 */ 文档注释-JavaDos: /** 注释 */标识符首字母以字母(AZ或az)和($或_) 开始标识符是大小写敏感(特别注意大小写) ![](C:\Users\HAIER\Pic…

[Python刷题记录]-只出现一次的数字-异或位运算-简单

[Python刷题记录]-只出现一次的数字-异或位运算-简单链接:136. 只出现一次的数字 - 力扣(LeetCode) 1 class Solution(object):2 def singleNumber(self, nums):3 """4 :type n…

安装 PySide2/PySide6/PyQt5/PyQt6

安装 PySide2/PySide6: pip install pyside2 pip install pyside6 安装 PyQt5/PyQt6: pip install pyqt5-tools pip install pyqt6-tools 即可同时安装 PyQt5/PyQt6 和一些重要的工具,比如 Qt Designer。

【Agent】 ACE(Agentic Context Engineering)源码阅读笔记---(3)关键创新

【Agent】 ACE(Agentic Context Engineering)源码阅读笔记---(3)关键创新 目录【Agent】 ACE(Agentic Context Engineering)源码阅读笔记---(3)关键创新0x00 概要0x01 增量式 Delta 更新1.1 原理1.2 源码解读0…

在Mac中用vscode写java

在Mac中用vscode写java下载jdk和vscode 进入官网 jdk vscode 找到对应的版本进行下载 我是MacBook Air m4芯片,适配arm系统 点击下载即可 安装 点击dmg安装 在vscode中安装插件 Chinese语言扩展包 Extension Pack for…