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
目录
相关文章
|
6天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
4天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
13 2
|
4天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
15 2
|
7天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
4天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
12 4
|
4天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
17 1
|
6天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
17天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
32 3
|
18天前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
26 3
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
130 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库