• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    go语言内存泄漏的常见形式

    发布者: 琛瑞6678 | 发布时间: 2025-8-14 13:33| 查看数: 70| 评论数: 0|帖子模式

    go语言内存泄漏



    子字符串导致的内存泄漏

    使用自动垃圾回收的语言进行编程时,通常我们无需担心内存泄漏的问题,因为运行时会定期回收未使用的内存。但是如果你以为这样就完事大吉了,哪里就大错特措了。
    因为,虽然go中并未对字符串时候共享底层内存块进行规定,但go语言编译器/运行时默认情况下允许字符串共享底层内存块,直到原先的字符串指向的内存被修改才会进行写时复制,这是一个很好的设计,既能节省内存,又能节省CPU资源,但有时也会导致"内存泄漏"。
    例如如下代码,一旦调用demo就会导致将近1M内存的泄漏,因为s0只使用了50字节,但是会导致1M的内存一直无法被回收,这些内存会一直持续到下次s0被修改的时候才会被释放掉。
    1. var s0 string // a package-level variable

    2. // A demo purpose function.
    3. func f(s1 string) {
    4.         s0 = s1[:50]
    5.         // Now, s0 shares the same underlying memory block
    6.         // with s1. Although s1 is not alive now, but s0
    7.         // is still alive, so the memory block they share
    8.         // couldn't be collected, though there are only 50
    9.         // bytes used in the block and all other bytes in
    10.         // the block become unavailable.
    11. }

    12. func demo() {
    13.         s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
    14.         f(s)
    15. }
    复制代码
    为了避免这种内存泄漏,我们可以使用[]byte来替代原先的1M大小的内存,不过这样会有两次50字节的内存重复
    1. func f(s1 string) {
    2.         s0 = string([]byte(s1[:50]))
    3. }
    复制代码
    当然我们也可以利用go编译器的优化来避免不必要的重复,只需要浪费一个字节内存就行
    1. func f(s1 string) {
    2.         s0 = (" " + s1[:50])[1:]
    3. }
    复制代码
    上述方法的缺点是编译器优化以后可能会失效,并且其他编译器可能无法提供该优化
    避免此类内存泄漏的第三种方法是利用 Go 1.10 以来支持的
    1. strings.Builder
    复制代码
    1. import "strings"

    2. func f(s1 string) {
    3.         var b strings.Builder
    4.         b.Grow(50)
    5.         b.WriteString(s1[:50])
    6.         s0 = b.String()
    7. }
    复制代码
    从 Go 1.18 开始,
    1. strings
    复制代码
    标准库包中新增了
    1. Clone
    复制代码
    函数,这成为了完成这项工作的最佳方式。

    子切片导致的内存泄漏

    同样场景下,切片也会导致内存的浪费
    与子字符串类似,子切片也可能导致某种内存泄漏。在下面的代码中,调用
    1. g
    复制代码
    函数后,保存
    1. s1
    复制代码
    元素的内存块所占用的大部分内存将会丢失(如果没有其他值引用该内存块)。
    1. var s0 []int

    2. func g(s1 []int) {
    3.         // Assume the length of s1 is much larger than 30.
    4.         s0 = s1[len(s1)-30:]
    5. }
    复制代码
    如果我们想避免这种内存泄漏,我们必须复制
    1. s0
    复制代码
    的 30 个元素,这样
    1. s0
    复制代码
    的活跃性就不会阻止收集承载
    1. s1
    复制代码
    元素的内存块。
    1. func g(s1 []int) {
    2.         s0 = make([]int, 30)
    3.         copy(s0, s1[len(s1)-30:])
    4.         // Now, the memory block hosting the elements
    5.         // of s1 can be collected if no other values
    6.         // are referencing the memory block.
    7. }
    复制代码
    未重置子切片指针导致的内存泄漏

    在下面的代码中,调用
    1. h
    复制代码
    函数后,为切片
    1. s
    复制代码
    的第一个和最后一个元素分配的内存块将丢失。
    1. func h() []*int {
    2.         s := []*int{new(int), new(int), new(int), new(int)}
    3.         // do something with s ...
    4.         // 返回一个从1开始,不能到索引3的新切片, 也就是 s[1], s[2]
    5.         return s[1:3:3]
    6. }
    复制代码
    只要返回的切片仍然有效,它就会阻止收集
    1. s
    复制代码
    的任何元素,从而阻止收集为
    1. s
    复制代码
    的第一个和最后一个元素引用的两个
    1. int
    复制代码
    值分配的两个内存块。
    如果我们想避免这种内存泄漏,我们必须重置丢失元素中存储的指针。
    1. func h() []*int {
    2.         s := []*int{new(int), new(int), new(int), new(int)}
    3.         // do something with s ...

    4.         // Reset pointer values.
    5.         s[0], s[len(s)-1] = nil, nil
    6.         return s[1:3:3]
    7. }
    复制代码
    挂起Goroutine导致的内存泄漏

    有时,Go 程序中的某些 goroutine 可能会永远处于阻塞状态。这样的 goroutine 被称为挂起的 goroutine。Go 运行时不会终止挂起的 goroutine,因此为挂起的 goroutine 分配的资源(以及它们引用的内存块)永远不会被垃圾回收。
    Go 运行时不会杀死挂起的 Goroutine 有两个原因。一是 Go 运行时有时很难判断一个阻塞的 Goroutine 是否会被永久阻塞。二是我们有时会故意让 Goroutine 挂起。例如,有时我们可能会让 Go 程序的主 Goroutine 挂起,以避免程序退出。

    如果不停止time.Ticker也会导致内存泄漏

    1. time.Timer
    复制代码
    值不再使用时,它会在一段时间后被垃圾回收。但
    1. time.Ticker
    复制代码
    值则不然。我们应该在
    1. time.Ticker
    复制代码
    值不再使用时停止它。

    不正确地使用终结器会导致真正的内存泄漏

    为属于循环引用组的成员值设置终结器(finalizer)可能会阻止为该循环引用组分配的所有内存块被回收。这是真正的内存泄漏,不是某种假象。
    例如,在调用并退出以下函数后,分配给
    1. x
    复制代码
    1. y
    复制代码
    的内存块不能保证在未来的垃圾收集中被收集。
    1. func memoryLeaking() {
    2.         type T struct {
    3.                 v [1<<20]int
    4.                 t *T
    5.         }

    6.         var finalizer = func(t *T) {
    7.                  fmt.Println("finalizer called")
    8.         }

    9.         var x, y T

    10.         // The SetFinalizer call makes x escape to heap.
    11.         runtime.SetFinalizer(&x, finalizer)

    12.         // The following line forms a cyclic reference
    13.         // group with two members, x and y.
    14.         // This causes x and y are not collectable.
    15.         x.t, y.t = &y, &x // y also escapes to heap.
    16. }
    复制代码
    因此,请避免为循环引用组中的值设置终结器。

    延迟函数调用导致的某种资源泄漏

    非常大的延迟调用堆栈也可能会消耗大量内存,并且如果某些调用延迟太多,某些资源可能无法及时释放。
    例如,如果在调用以下函数时需要处理许多文件,那么在函数退出之前将有大量文件处理程序无法释放。
    1. func writeManyFiles(files []File) error {
    2.         for _, file := range files {
    3.                 f, err := os.Open(file.path)
    4.                 if err != nil {
    5.                         return err
    6.                 }
    7.                 defer f.Close()

    8.                 _, err = f.WriteString(file.content)
    9.                 if err != nil {
    10.                         return err
    11.                 }

    12.                 err = f.Sync()
    13.                 if err != nil {
    14.                         return err
    15.                 }
    16.         }

    17.         return nil
    18. }
    复制代码
    对于这种情况,我们可以使用匿名函数来封装延迟调用,以便延迟函数调用能够更早地执行。例如,上面的函数可以重写并改进为
    1. func writeManyFiles(files []File) error {
    2.         for _, file := range files {
    3.                 if err := func() error {
    4.                         f, err := os.Open(file.path)
    5.                         if err != nil {
    6.                                 return err
    7.                         }
    8.                         // The close method will be called at
    9.                         // the end of the current loop step.
    10.                         defer f.Close()

    11.                         _, err = f.WriteString(file.content)
    12.                         if err != nil {
    13.                                 return err
    14.                         }

    15.                         return f.Sync()
    16.                 }(); err != nil {
    17.                         return err
    18.                 }
    19.         }

    20.         return nil
    21. }
    复制代码
    当然不要犯以下错误,需要有些同学将需要延时调用的函数字节省略,导致资源泄漏
    1. _, err := os.Open(file.path)
    复制代码
    如果是http请求,还会导致服务端挤压大量的连接无法释放
    到此这篇关于go语言内存泄漏的常见形式的文章就介绍到这了,更多相关go语言内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:互联网
    免责声明:如果侵犯了您的权益,请联系站长(1277306191@qq.com),我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    浏览过的版块

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表