golang面经——内存相关模块

golang的内存布局

Go 复制代码
+----------------------------------------+  ← 高地址(如 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 泄漏

举例:

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

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

举例:

Go 复制代码
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 可能无法释放。

Go 复制代码
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 不匹配

Go 复制代码
func leak6() {
    var wg sync.WaitGroup
    wg.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 持有引用)

Go 复制代码
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 中 Stop
    go 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 无法回收。

Go 复制代码
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.方法内返回局部变量指针。

Go 复制代码
func createInt() *int {
    x := 42
    return &x  // x 逃逸到堆
}

2.向 channel 发送指针数据。

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

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

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

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

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

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

Go 复制代码
// main.go
package main

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

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

Go 复制代码
func main() {
    x := 100
    var i interface{} = x // 赋值给 interface{}
    _ = i
}

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

Go 复制代码
go build -gcflags="-m -l" main.go
  • -m:打印逃逸分析和内联决策;
  • -l:禁止内联(避免干扰分析)。
Go 复制代码
// main.go
func f() *int {
    x := 1
    return &x
}
Go 复制代码
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也可以看是乐观锁);并发优化这个在业务开发里面比较少用。

相关推荐
米羊12118 分钟前
已有安全措施确认(上)
大数据·网络
Fcy64823 分钟前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满25 分钟前
Linux怎么查看最新下载的文件
linux·运维·服务器
主机哥哥1 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
ManThink Technology1 小时前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
珠海西格电力科技2 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
QT.qtqtqtqtqt2 小时前
未授权访问漏洞
网络·安全·web安全
释怀不想释怀2 小时前
Linux环境变量
linux·运维·服务器
zzzsde2 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器
半壶清水3 小时前
[软考网规考点笔记]-软件开发、项目管理与知识产权核心知识与真题解析
网络·笔记·压力测试