Go 语言内存逃逸案例

简介: Go 语言内存逃逸案例

介绍

在「Go 语言逃逸分析」中,我们了解到内存分配的相关知识,栈空间分配开销小,堆空间分配开销大。

Go 语言编译器可以通过逃逸分析决定内存分配到栈空间或堆空间。但是,分配到栈空间的对象在某些情况中会逃逸到堆空间。我们可以使用 Go 工具链查看对象是否发生内存逃逸。

为了提升 Go 应用程序的性能,我们应该避免 Go 应用程序中出现内存逃逸的现象,本文我们介绍 Go 语言内存逃逸的几种典型案例。

内存逃逸案例

指针逃逸

示例代码:

func main() {
 pointerEscape(1, 2)
}
// pointerEscape 指针逃逸
func pointerEscape(a, b int) *int {
 sum := a + b
 return &sum
}

输出结果:

go build --gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:9:2: sum escapes to heap:
./main.go:9:2:   flow: ~r0 = &sum:
./main.go:9:2:     from &sum (address-of) at ./main.go:10:9
./main.go:9:2:     from return &sum (return) at ./main.go:10:2
./main.go:9:2: moved to heap: sum

阅读上面这段代码,我们创建一个函数 pointerEscape,函数内部创建一个局部变量 sum,返回结果是该变量的指针。

通过执行 go build --gcflags '-m -m -l' main.go 的输出结果,我们发现函数中定义的局部变量 sum 逃逸到堆空间,这就是所谓的指针逃逸。

函数 pointerEscape 的局部变量 sum 本来应该在函数结束时被回收,但是在 main 函数中会继续使用 sum 变量的内存地址,导致变量 sum 被逃逸到堆上。

如果想要避免示例函数的返回结果出现内存逃逸,可以使用值类型的返回结果,这样就带来另外一个问题。

如果返回结果是一个比较大的变量,比如返回结果是较大的结构体类型的变量,我们使用值类型将会造成比较大的内存占用。

所以,我们在实际项目开发中,需要根据实际情况,合理使用返回结果的类型。

动态类型逃逸

示例代码:

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

输出结果:

go run -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:6:14: "hello world" escapes to heap:
./main.go:6:14:   flow: {storage for ... argument} = &{storage for "hello world"}:
./main.go:6:14:     from "hello world" (spill) at ./main.go:6:14
./main.go:6:14:     from ... argument (slice-literal-element) at ./main.go:6:13
./main.go:6:14:   flow: {heap} = {storage for ... argument}:
./main.go:6:14:     from ... argument (spill) at ./main.go:6:13
./main.go:6:14:     from fmt.Println(... argument...) (call parameter) at ./main.go:6:13
./main.go:6:13: ... argument does not escape
./main.go:6:14: "hello world" escapes to heap

阅读上面这段代码,我们在 main 函数中,使用 fmt.Println() 打印字符串 hello world

通过执行 go run -gcflags '-m -m -l' main.go 的输出结果,我们发现使用 fmt.Println() 打印的字符串 hello world 逃逸到堆上,这就是所谓的动态类型逃逸。

因为 fmt.Println() 接收的参数是空接口类型,Go 编译器无法确定入参变量的具体类型,所以此类情况变量也会逃逸到堆上。

03

总结

本文我们介绍两个典型的内存逃逸的案例,除此之外,以下几种情况,也会发生内存逃逸。

  • 发送指针或带有指针的值到 channel 中。
  • 在一个切片上存储指针或带指针的值。
  • slice 的底层数组被重新分配(append 超出其容量时)。

感兴趣的读者朋友们,可以自行编写上述几种情况的示例代码,验证是否会发生内存逃逸。

推荐阅读:

参考资料:

  1. http://npat-efault.github.io/programming/2016/10/10/escape-analysis-and-interfaces.html
  2. http://www.wingtecher.com/themes/WingTecherResearch/assets/papers/ICSE20.pdf
目录
相关文章
|
11月前
|
Linux Go iOS开发
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
本文详解Go语言打包与跨平台编译技巧,涵盖`go build`命令、多平台构建、二进制优化及资源嵌入(embed),助你将项目编译为无依赖的独立可执行文件,轻松实现高效分发与部署。
1655 162
|
11月前
|
存储 前端开发 JavaScript
Go语言实战案例-项目实战篇:编写一个轻量级在线聊天室
本文介绍如何用Go语言从零实现一个轻量级在线聊天室,基于WebSocket实现实时通信,支持多人消息广播。涵盖前后端开发、技术选型与功能扩展,助你掌握Go高并发与实时通信核心技术。
1050 158
|
数据采集 Go API
Go语言实战案例:使用context控制协程取消
本文详解 Go 语言中 `context` 包的使用,通过实际案例演示如何利用 `context` 控制协程的生命周期,实现任务取消、超时控制及优雅退出,提升并发程序的稳定性与资源管理能力。
621 152
|
安全 Go 开发者
Go语言实战案例:使用sync.Mutex实现资源加锁
在Go语言并发编程中,数据共享可能导致竞态条件,使用 `sync.Mutex` 可以有效避免这一问题。本文详细介绍了互斥锁的基本概念、加锁原理及实战应用,通过构建并发安全的计数器演示了加锁与未加锁的区别,并封装了一个线程安全的计数器结构。同时对比了Go中常见的同步机制,帮助开发者理解何时应使用 `Mutex` 及其注意事项。掌握 `Mutex` 是实现高效、安全并发编程的重要基础。
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
Go 开发者
Go语言实战案例:使用select监听多个channel
本文为《Go语言100个实战案例 · 网络与并发篇》第5篇,详解Go并发核心工具`select`的使用。通过实际案例讲解如何监听多个Channel、实现多任务处理、超时控制和非阻塞通信,帮助开发者掌握Go并发编程中的多路异步事件处理技巧。
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
机器学习/深度学习 存储 算法
Go语言实战案例-广度优先遍历BFS
广度优先遍历(BFS)是一种层级展开的搜索策略,常用于树与图的遍历、最短路径查找、二维数组中的感染扩散等问题。它借助队列实现,优先访问当前层所有节点,再进入下一层,适用于寻找最短路径、层序遍历、岛屿问题等场景。