在 Go 语言中,虽然有垃圾收集器(GC)管理内存,但一些编程错误仍然可能导致内存泄漏。这些内存泄漏通常不是由于传统意义上的未释放内存,而是由于程序不合理的持有内存资源,使得 GC 无法回收不再需要的数据。以下是常见的导致 Go 内存泄漏的场景:
### 1\. **Goroutine 泄漏**
如果 Goroutine 被创建但无法正常退出或被阻塞,可能会一直占用内存资源,造成 Goroutine 泄漏。
#### 场景示例:
~~~
go复制代码func startGoroutine(ch chan int) {
go func() {
for {
select {
case v := <-ch:
fmt.Println(v)
}
}
}()
}
~~~
* 如果 `ch` 通道关闭后没有关闭 Goroutine,或者 Goroutine 一直在等待没有数据到来的通道,那么 Goroutine 会一直运行或阻塞,导致内存无法回收。
**解决方法**:确保 Goroutine 能够在不需要时退出,通常可以通过关闭通道或者增加超时机制来控制。
~~~
go复制代码func startGoroutine(ch chan int, done chan struct{}) {
go func() {
for {
select {
case v := <-ch:
fmt.Println(v)
case <-done:
return
}
}
}()
}
~~~
### 2\. **持有大对象的引用**
如果某个数据结构(如切片、映射、结构体)持有大对象的引用,但程序实际上不再需要这些对象的某些部分,GC 将无法回收这部分不需要的数据。
#### 场景示例:
~~~
go复制代码func processData() {
data := make([]byte, 1000) // 分配了 1000 字节
// 假设我们只需要其中一部分
slice := data[:10] // 只使用了 10 字节
_ = slice
}
~~~
* 虽然只使用了 `data` 的一部分(`slice`),但原始的 `data` 仍然在内存中占据 1000 字节,无法被 GC 回收。
**解决方法**:如果不再需要原始的大对象,可以拷贝需要的数据到新的切片中来释放多余的内存。
~~~
go复制代码func processData() {
data := make([]byte, 1000)
slice := make([]byte, 10)
copy(slice, data[:10]) // 只保留需要的数据
}
~~~
### 3\. **全局变量或单例模式**
Go 中的全局变量或长生命周期的对象(如单例模式中的数据)会导致内存长时间无法释放,因为它们的生命周期与程序一致。只要这些变量没有被合理地清理,它们所占用的内存就会持续存在。
#### 场景示例:
~~~
go复制代码var cache = map[string]string{}
func addToCache(key, value string) {
cache[key] = value
}
~~~
* `cache` 是全局变量,如果不及时清理无用的数据,内存将不断增长,最终可能导致内存泄漏。
**解决方法**:定期清理全局变量中的无用数据,或者为其设置合理的淘汰策略。
### 4\. **切片的容量增长**
在 Go 中,切片的容量可能比实际使用的大小要大得多,特别是在反复扩展切片的过程中。如果不注意控制切片的容量增长,可能会造成内存浪费。
#### 场景示例:
~~~
go复制代码func appendData() {
data := make([]int, 0, 100)
for i := 0; i < 1000; i++ {
data = append(data, i)
}
}
~~~
* `data` 的容量会随着不断 `append` 增长,最终占用的内存可能远大于需要的内存,而原来的较小容量的切片部分仍然无法被回收。
**解决方法**:在确定不再需要额外容量时,可以使用 `copy` 或 `append` 函数截断切片,以释放不必要的容量。
~~~
go复制代码func appendData() {
data := make([]int, 0, 100)
for i := 0; i < 1000; i++ {
data = append(data, i)
}
data = append([]int(nil), data...) // 重建切片以清理多余的容量
}
~~~
### 5\. **未关闭的通道**
未关闭的通道也可能导致内存泄漏,特别是当通道中的发送或接收 Goroutine 永远阻塞时,这样会导致 Goroutine 泄漏和内存积累。
#### 场景示例:
~~~
go复制代码func sendToChannel(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 通道未关闭,可能导致阻塞
}
~~~
**解决方法**:当不再需要时,及时关闭通道,避免内存泄漏和 Goroutine 阻塞。
~~~
go复制代码func sendToChannel(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // 正确关闭通道
}
~~~
### 6\. **使用不必要的保持引用**
如果某个对象被其他对象持有引用,GC 将无法回收这个对象。常见的情况是某些缓存结构不合理地保留了对大量数据的引用。
#### 场景示例:
~~~
go复制代码type Node struct {
value int
next *Node
}
func createList() *Node {
head := &Node{value: 1}
current := head
for i := 2; i <= 1000; i++ {
current.next = &Node{value: i}
current = current.next
}
return head
}
~~~
* 如果链表结构中的某个节点被持有,即使链表已经不再需要,整个链表的内存也无法被回收。
**解决方法**:避免不必要的保持引用,及时释放不再使用的数据结构。
### 7\. **长生命周期的 goroutine 队列或池**
长生命周期的队列或者池会导致一些 Goroutine 无法及时释放,特别是当队列或池中的任务没有被及时处理完毕时。
#### 场景示例:
~~~
go复制代码var taskQueue = make(chan func(), 100)
func worker() {
for task := range taskQueue {
task()
}
}
~~~
* 如果 `taskQueue` 长期未被清空,或者没有关闭,这些 Goroutine 可能会一直阻塞。
**解决方法**:为任务队列设定合理的大小,并确保在程序终止时关闭通道或清理任务。
* * *
### 总结
尽管 Go 语言有自动垃圾回收机制,以下情况仍然可能导致内存泄漏:
* Goroutine 泄漏(未正常退出或阻塞)。
* 持有大对象的引用。
* 全局变量或单例模式不及时清理。
* 切片容量增长导致内存浪费。
* 未关闭的通道。
* 使用不必要的保持引用。
* 长生命周期的 Goroutine 队列或池。
为避免内存泄漏,开发者需要在设计程序时小心管理对象的生命周期、及时释放资源,并使用工具如 `pprof` 监控内存使用情况。
- Golang
- 切片 slice
- 数组和切片的区别
- 左闭右开
- make([]int, 5) 和 make([]int, 0, 5) 区别
- 切片非线程安全,并发操作为啥不会像map一样报错
- []struct{} 如何遍历
- 切片如何删除某个元素
- append 一个nil 切片
- 哈希表 map
- 并发操作
- 并发写报错
- 并发读不会报错
- 并发读有写报错
- 并发迭代有写报错
- 自制并发安全字典
- 官方并发安全字典
- 对未初始化的 map 进行赋值操作
- map的底层
- 无序输出
- 等量扩容
- 实现集合
- map的key可以使哪些值
- 协程 go
- 协程相关阅读
- 进程、线程、协程
- 协程 (捕获异常 和 协程池)
- GPM 模型
- CSP模型
- channel
- channel 相关操作
- 交替打印
- 如何让channel 只能接收/只能发送
- channel 常见报错
- channel 死锁
- nil channel 和 已关闭的 channel
- 使用 select 来多路复用 channel
- channel 的使用
- 接口和结构体
- 简单使用
- 两个结构体能否比较
- 工厂模式
- 概念
- 简单工厂
- 方法工厂
- 堆和栈,值类型和引用类型,内存逃逸,垃圾回收
- 栈和堆
- 内存逃逸
- 值类型和引用类型
- 垃圾回收方式
- 性能优化分析工具 pprof
- golang 代码片段
- 片段一 defer
- 片段二 channel
- Golang 相关
- Golang 相关阅读
- Golang 1-10
- make 和 new 的区别
- 使用指针的场景
- Go语言的context包
- 位运算
- Copy 是浅拷贝还是深拷贝
- init 函数 和 sync.Once
- select 多路复用
- Golang 其它
- MongoDB
- 可比较类型 与 可转json 类型
- Gorm
- 面向对象和面向过程
- go语言实现-面向对象
- go语言实现-面向过程
- 限流,熔断,降级
- 了解
- 熔断配置
- 熔断例子
- 服务降级
- github.com/alibaba/sentinel-golang
- 互斥锁 读写锁 原子锁
- 为什么需要锁
- 互斥锁
- 读写锁
- 原子锁
- 互斥锁性能对比
- 原子锁性能对比
- 互斥锁 or 原子锁?
- 条件锁
- 计数器
- GoFrame
- GF1.16版本
- 修改使用的表
- 按天、周、月、年
- GoFrame 文档
- 配置文件
- 生成脚本
- 排序算法
- 相关排序
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 归并排序
- 堆排序
- 数据库
- 分布式怎么保证线程安全
- 数据库实现方式
- 基于表记录
- 乐观锁
- 悲观锁
- Redis实现方式
- Zookeeper实现方式
- Mysql 相关
- group_concat
- 索引优化
- 索引优化1
- 定期分析和优化索引
- 覆盖索引
- 组合索引
- 聚簇索引和非聚簇索引
- 索引类型与方式、聚簇与非聚簇索引
- 事务特征和隔离级别
- 查询优化
- mysql自增表插入数据时,Id不连续问题
- InnoDB引擎 和 MyISAM引擎区别
- 锁
- 悲观锁和乐观锁
- 查询,更新,插入语句
- 什么是死锁
- 怎么处理死锁
- MySQL 隔离级别
- 事务特征
- 隔离级别
- 废弃3
- 索引
- 索引类型和方式、聚簇和非聚簇索引(上)
- 索引类型和方式、聚簇和非聚簇索引(下)
- 回表、覆盖索引、最左前缀、联合索引、索引下推、索引合并
- Mysql 优化
- 索引的原理
- 千万级表修改表结构
- Redis
- 获取随机三条数据
- Redis 持久化方式
- 全量模式 RDB 冷备份(内存快照)
- 增量模式 AOF 热备份(文件追加)
- 过期key的删除策略、内存淘汰机制
- 数据结构
- 位图
- 网络
- 网络相关
- 游戏同步方式:帧同步和状态同步
- Websocket
- OSI模型
- TCP 与 UDP
- 三次握手四次挥手
- Http 状态码
- 1xx(信息性状态码)
- 101 服务端代码
- 101 客户端代码
- 2xx(成功状态码)
- 3xx(重定向状态码)
- 302 服务端代码
- 302 客户端代码
- 4xx(客户端错误状态码)
- 5xx(服务器错误状态码)
- 如何排查接口问题
- 网络请求和响应过程
- time_wait
- keep-alive
- http 和 rpc 的区别
- I/O多路复用 select和poll
- too many open file
- 其它技术
- git 相关操作
- 修改提交备注
- 多个提交合并成一个提交
- 回退版本
- 小程序和公众号
- 消息模板
- 获取code
- 静默登录
- 其它技术相关
- C盘空间不足
- 生成式人工智能AIGC
- 共享文件
- 接口文档, mock提供测试数据
- 抓包工具
- Python
- 安装包失败
- 自动化测试 Scrapy
- AIGC:人工智能生成内容
- PHP
- xhprof 性能分析
- 一键安装
- 哈希冲突的解决方式
- 链地址法(拉链法)
- 开放地址法
- 再哈希
- 概念1
- Nginx
- 负载均衡方式
- 加密解密
- 简单了解
- 签名算法例子
- 码例子1
- 代码例子2
- Linux
- netstat (用于查看和管理网络连接和路由表)
- ps 用于查看和管理进程
- ab 压测
- nohup 守护进程
- lsof (List Open File 获取被进程打开文件的信息)
- tail 查看日志
- 各类linux同步机制
- Socket 服务端的实现,select 和epoll的区别?
- scp 传输,awk 是一个强大的文本分析工具
- pidof
- 项目
- 棋牌
- 牌的编码
- 出牌规则
- 洗牌
- 股票
- 股票知识
- 龙虎榜数据缓存方式
- 单日龙虎榜数据
- 单只股票的历史上榜
- 遇到的问题
- 浮点数精度问题
- Mysql Sum 精度问题(float, double精度问题)
- 分页问题(数据重复)
- 工具包
- v3
- common.go
- common_test.go
- customized.go
- customized_test.go
- slice.go
- slice_test.go
- time.go
- time_test.go
- v4
- common.go
- common_test.go
- customized.go
- customized_test.go
- slice.go
- time.go
- time_test.go
- 相关阅读
- 切片 slice
- 集合 map
- 协程 goroutine
- 通道 channel
- 相关阅读 s
- pyTorch
- defer
- 内存泄漏