首页
Preview

深入探究Go高级主题——垃圾回收器

Go中的垃圾回收简介

根据wikipedia source,垃圾回收(GC)是一种自动内存管理形式。垃圾收集器尝试回收程序分配但不再被引用的内存,这种内存称为垃圾。

Go的垃圾回收是一种自动释放程序不再需要的内存的机制。 Go运行时使用垃圾收集器定期扫描程序的堆(对象分配的内存区域),查找不再可达的对象。一旦垃圾收集器确定对象不再可达,它将释放与该对象关联的内存,并使其可供重用。

Go的垃圾收集器使用并发的“停顿式”算法。这意味着当垃圾收集器正在运行时,程序将暂停并无法继续执行。暂停的持续时间通常非常短,用户不会注意到。

值得注意的是,Go的指针语法部分借鉴了C和C ++。由于Go有垃圾收集器,大多数内存管理的痛苦都被消除了。此外,在C和C ++中可以使用指针的一些技巧,包括指针算术,在Go中是不允许的。

Go垃圾收集器使用称为“跟踪”的技术来确定哪些对象仍然可达。垃圾收集器从一组“根”对象开始,例如全局变量,然后“跟踪”从这些对象到其他对象的指针,标记所有可达的对象。任何未标记为可达的对象都被视为垃圾,并且可以进行收集。

Go还使用称为“分代”的垃圾收集技术,这意味着垃圾收集器将堆分成不同的代,并首先专注于从较旧的代中收集对象。这种策略在大多数情况下都会产生更好的性能,因为大多数对象的生命周期很短,将很快被收集。

Golang垃圾收集器如何影响性能?

Go中的垃圾收集器可能会影响程序的性能,具体取决于其使用方式和配置方式。以下是垃圾收集器可能影响性能的几种方式:

  • GC暂停时间: 在Go中进行垃圾收集是与程序执行并发进行的。但是,当垃圾收集器正在运行时,程序的执行会有短暂的暂停。如果程序正在运行性能敏感的工作负载(例如实时应用程序),这些暂停可能会明显。
  • 内存分配: 垃圾收集器可能会导致额外的内存分配和释放,这可能会增加程序的内存使用量并导致缓存未命中,从而导致CPU使用率增加。
  • CPU使用率: 垃圾收集器使用一些CPU时间来识别和回收不可达对象,如果程序正在运行以CPU为限制的工作负载,则可能会导致性能问题。
  • GC触发阈值: 垃圾收集器的触发阈值可以通过运行时进行配置。 GOGC变量设置初始垃圾收集目标百分比。如果将触发阈值设置得过低,则垃圾收集器可能会过于频繁地运行,从而导致CPU使用率和GC暂停时间增加。如果将触发阈值设置得过高,则程序可能会在垃圾收集器运行之前耗尽内存。
  • 内存压力: 如果程序承受内存压力,则可能会导致性能下降。尽管Go垃圾收集器在释放内存方面效率很高,但如果程序的内存使用量过高,则可能会导致垃圾收集器频繁运行,并且程序可能会耗尽内存。

Go的垃圾回收器如何工作?

示例1:Go中的内存分配是如何工作的

以下是一个简单的Go示例,在声明变量s时,字符串对象“hello, world"分配了内存。只要变量s在范围内,字符串对象就被视为可达并且不会被垃圾回收。一旦变量s超出范围,字符串对象就不再可达,可以进行垃圾回收。

package main

import "fmt"

func main() {
 // Allocate memory for a new string object
 s := "hello, world"

 // The variable s is still in scope, so the string object it references is considered reachable
 fmt.Println(s)

 // Once the variable s goes out of scope, the string object it references is no longer reachable
 // and will be eligible for garbage collection
}

示例2:使用runtime.GC()手动触发垃圾收集

在此示例中,我们使用runtime.GC()手动触发垃圾收集:

package main

import (
 "fmt"
 "runtime"
)

func main() {
 // Allocate some memory for the program to use
 s := make([]string, 0, 100000)
 for i := 0; i < 100000; i++ {
  s = append(s, "hello, world")
 }

 // Print the initial memory usage
 var m runtime.MemStats
 runtime.ReadMemStats(&m)
 fmt.Println("Initial HeapAlloc: ", m.HeapAlloc)

 // Trigger the garbage collector
 runtime.GC()

 // Print the memory usage after the garbage collector has run
 runtime.ReadMemStats(&m)
 fmt.Println("After GC HeapAlloc: ", m.HeapAlloc)

 // Release the memory
 s = nil
 // Trigger the garbage collector
 runtime.GC()
 // Print the memory usage after the garbage collector has run
 runtime.ReadMemStats(&m)
 fmt.Println("After release HeapAlloc: ", m.HeapAlloc)
}

此程序使用runtime包手动触发垃圾收集,然后打印垃圾回收之前和之后的堆分配。

输出:

# go run main.go 
Initial HeapAlloc:  1654512
After GC HeapAlloc:  37872
After release HeapAlloc:  37872

此程序演示了如何使用runtime包检查和释放Go程序分配的内存。

首先,创建了一个包含100,000个字符串的切片,并附加了“hello, world”。这会创建大量占用内存的分配。

然后,程序使用runtime.MemStats结构和runtime.ReadMemStats()函数打印初始内存使用情况。这提供有关程序内存使用情况的信息。

接下来,使用runtime.GC()触发垃圾收集。这将使程序回收不再使用的任何内存。垃圾收集器运行后,程序再次打印内存使用情况。

最后,将切片设置为nil以释放内存,再次触发垃圾收集器,然后再次打印内存使用情况。

如果你想知道为什么要使用runtime.ReadMemStats(&m)而不是runtime.ReadMemStats(m)?

在Go中,所有函数参数都是按值传递的。这意味着当我们调用函数时,会创建参数的副本,并且函数使用该副本。

如果我们要修改变量的原始值,我们需要传递指向该变量的指针,这样我们就可以直接访问和修改它的值。

runtime.ReadMemStats(&m)的情况下,m是类型为runtime.MemStats的变量,我们想将指向此变量的指针传递给ReadMemStats函数,以便它可以修改其值。因此,我们使用运算符获取m的地址并将其作为指针传递。这样,函数就可以访问和修改m的原始值。

如果我们直接将m传递给函数,如此:runtime.ReadMemStats(m),我们将传递m的值的副本而不是指向它的指针。这将不允许函数修改m的原始值,并且我们将无法获得所需的结果。## 示例-3:在垃圾回收器执行前后显示堆统计信息

以下是另一个示例,展示了如何使用runtime.ReadMemStats()函数检索有关程序内存使用情况的统计信息并监视垃圾回收器:

package main

import (
 "fmt"
 "runtime"
)

func main() {
 var m runtime.MemStats

 // Print statistics before the slice is released
 runtime.ReadMemStats(&m)
 fmt.Println("HeapAlloc: ", m.HeapAlloc)
 fmt.Println("HeapIdle: ", m.HeapIdle)
 fmt.Println("HeapReleased: ", m.HeapReleased)
 fmt.Println("NumGC: ", m.NumGC)
 fmt.Println("-----------")

 // Allocate some memory for the program to use
 s := make([]string, 0, 100000)
 for i := 0; i < 100000; i++ {
  s = append(s, "hello, world")
 }

 // Print statistics after the slice is released
 runtime.ReadMemStats(&m)
 fmt.Println("HeapAlloc: ", m.HeapAlloc)
 fmt.Println("HeapIdle: ", m.HeapIdle)
 fmt.Println("HeapReleased: ", m.HeapReleased)
 fmt.Println("NumGC: ", m.NumGC)
 fmt.Println("-----------")
 // Release the memory
 s = nil

 // Manually trigger garbage collector
 runtime.GC()
 runtime.ReadMemStats(&m)
 fmt.Println("HeapAlloc: ", m.HeapAlloc)
 fmt.Println("HeapIdle: ", m.HeapIdle)
 fmt.Println("HeapReleased: ", m.HeapReleased)
 fmt.Println("NumGC: ", m.NumGC)
 fmt.Println("-----------")
}

在此示例中,我们创建了一个大型字符串切片,该切片可以保留一些内存,然后使用runtime.ReadMemStats()函数检索内存统计信息。统计信息包括当前已分配的内存量(HeapAlloc)、当前未使用和可用的内存量(HeapIdle)、释放到操作系统的内存量(HeapReleased)和垃圾回收运行的次数(NumGC)。

之后,我们将切片设置为nil并允许垃圾回收器回收内存。然后我们调用runtime.GC()手动触发垃圾回收器并再次检索统计信息。

输出:

# go run main.go 

HeapAlloc:  48048
HeapIdle:  3719168
HeapReleased:  3686400
NumGC:  0
-----------
HeapAlloc:  1654104
HeapIdle:  2072576
HeapReleased:  2072576
NumGC:  0
-----------
HeapAlloc:  37648
HeapIdle:  3710976
HeapReleased:  2039808
NumGC:  1
-----------

还要注意,垃圾回收运行的时间不能保证,在看到预期结果之前我们可能需要等待一段时间或手动触发垃圾回收器使用runtime.GC()

我们还可以使用这些统计信息来检测潜在的内存泄漏和性能问题,同时我们可以使用HeapAllocHeapReleased检查程序是否正确释放内存。

一些知识:

在Go中,内存由垃圾回收器自动管理,垃圾回收器会释放程序不再需要的内存。当一个变量不再需要时,将其设置为nil可以使垃圾回收器回收为该变量分配的内存。

另一方面,手动调用runtime.GC()会立即触发垃圾回收器运行。在某些情况下,这可能很有用,例如如果你已经分配了大量内存,而这些内存不再需要,想要快速释放它们。

释放内存和触发垃圾回收器之间的区别在于,释放内存将切片设置为nil,这允许垃圾回收器在未来的某个时间回收内存。手动触发垃圾回收器强制垃圾回收器立即运行,这意味着任何未引用的内存都将立即被回收。

总之,将变量设置为nil是告诉垃圾回收器它所引用的内存可以被回收的一种方式,而调用runtime.GC()是强制垃圾回收器立即运行并释放可能尚未释放的内存的一种方式。

示例-4:使用GODEBUG调试Go垃圾回收器

GODEBUG是变量控制器,用于调试Go运行时中的变量。该变量包含一个由逗号分隔的name=val键值对列表。这些命名变量用于调整二进制文件将返回的调试信息的输出。以下是另一段代码,它尝试有效地释放未使用的内存:

package main

import (
 "fmt"
 "runtime"
 "time"
)

func printStats(mem runtime.MemStats) {
 runtime.ReadMemStats(&mem)
 fmt.Println("mem.Alloc:", mem.Alloc)
 fmt.Println("mem.TotalAlloc:", mem.TotalAlloc)
 fmt.Println("mem.HeapAlloc:", mem.HeapAlloc)
 fmt.Println("mem.NumGC:", mem.NumGC)
 fmt.Println("-----")
}

func main() {
 var mem runtime.MemStats
 printStats(mem)

 for i := 0; i < 10; i++ {
  s := make([]byte, 100000000)
  if s == nil {
   fmt.Println("Operation failed!")
  }
 }
 printStats(mem)

 for i := 0; i < 10; i++ {
  s := make([]byte, 100000000)
  if s == nil {
   fmt.Println("Operation failed!")
  }
  time.Sleep(5 * time.Second)
 }
 printStats(mem)

}

我们使用for循环获取大量内存以触发垃圾回收的使用。

输出:

# go run main.go 
mem.Alloc: 48256
mem.TotalAlloc: 48256
mem.HeapAlloc: 48256
mem.NumGC: 0
-----
mem.Alloc: 100045200
mem.TotalAlloc: 1000128496
mem.HeapAlloc: 100045200
mem.NumGC: 9
-----
^Csignal: interrupt

因此,输出显示了与main.go程序使用的内存相关的属性信息。如果我们想获得更详细的输出,可以使用GODEBUG执行相同的工具,如下所示:

# GODEBUG=gctrace=1 go run main.go a
gc 1 @0.019s 1%: 0.014+2.4+0.001 ms clock, 0.014+0.33/0/0+0.001 ms cpu, 4->4->0 MB, 5 MB goal, 1 P
gc 2 @0.050s 3%: 0.027+5.1+0.002 ms clock, 0.027+0.37/1.0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 3 @0.089s 2%: 0.067+3.3+0.002 ms clock, 0.067+0.66/0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 4 @0.128s 2%: 0.032+2.6+0.003 ms clock, 0.032+0.82/0/0+0.003 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 5 @0.153s 2%: 0.046+5.1+0.002 ms clock, 0.046+0.81/0/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
gc 6 @0.175s 3%: 0.030+11+0.002 ms clock, 0.030+1.4/0.16/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
gc 7 @0.224s 2%: 0.027+2.4+0.003 ms clock, 0.027+0.63/0/0+0.003 ms cpu, 4->5->2 MB, 5 MB goal, 1 P
# command-line-arguments
gc 1 @0.004s 17%: 0.009+2.4+0.002 ms clock, 0.009+1.3/0/0+0.002 ms cpu, 4->6->5 MB, 5 MB goal, 1 P
gc 2 @0.036s 16%: 0.014+8.7+0.004 ms clock, 0.014+4.0/2.2/0+0.004 ms cpu, 9->9->8 MB, 11 MB goal, 1 P
mem.Alloc: 48128
mem.TotalAlloc: 48128
mem.HeapAlloc: 48128
mem.NumGC: 0
-----
gc 1 @0.007s 1%: 0.011+0.11+0.002 ms clock, 0.011+0.10/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 2 @0.054s 0%: 0.030+0.13+0.002 ms clock, 0.030+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 3 @0.106s 0%: 0.023+0.12+0.002 ms clock, 0.023+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 4 @0.141s 0%: 0.023+0.15+0.004 ms clock, 0.023+0.15/0/0+0.004 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 5 @0.185s 0%: 0.021+0.12+0.001 ms clock, 0.021+0.11/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 6 @0.221s 0%: 0.023+0.22+0.002 ms clock, 0.023+0.22/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 7 @0.269s 0%: 0.025+0.12+0.001 ms clock, 0.025+0.12/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 8 @0.311s 0%: 0.032+0.33+0.002 ms clock, 0.032+0.32/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 9 @0.350s 0%: 0.022+0.10+0.006 ms clock, 0.022+0.097/0/0+0.006 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 10 @0.390s 0%: 0.021+0.11+0.005 ms clock, 0.021+0.10/0/0+0.005 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
mem.Alloc: 100045256
mem.TotalAlloc: 1000128368
mem.HeapAlloc: 100045256
mem.NumGC: 9
-----

让我们理解这个输出:

示例-5:使用切片作为缓冲区

当从外部资源(如文件或网络连接)读取数据时,许多语言使用以下代码:

 r = open_resource()
    while r.has_data() {
      data_chunk = r.next_chunk()
      process(data_chunk)
    }
close(r)

这种模式的问题在于,每次我们通过while循环迭代时,即使每个数据块仅使用一次,我们也会分配另一个data_chunk。这会创建大量不必要的内存分配。虽然Go是一种垃圾回收语言,但编写惯用的Go意味着避免不必要的分配。我们创建一个字节切片作为缓冲区,用它从数据源读取数据,而不是每次从数据源返回一个新的分配:

// Open the file
file, err := os.Open(fileName)
if err != nil {
    return err
}

// Defer the file.Close() function call to ensure the file is always closed
defer file.Close()

// Create a byte slice to hold the data
data := make([]byte, 100)

// Loop over the file data in chunks of 100 bytes
for {
    // Read up to 100 bytes of data into the data slice
    count, err := file.Read(data)
    if err != nil {
        return err
    }

    // If no data was read, we're done
    if count == 0 {
        return nil
    }

    // Process the data slice up to the number of bytes read
    process(data[:count])
}

请记住,当我们将切片传递给函数时,不能更改其长度或容量,但可以更改当前长度的内容。在此代码中,我们创建了一个100字节的缓冲区,并在每次循环时将下一个字节块(最多100个)复制到切片中。然后我们将填充的部分缓冲区传递给process。

更多知识:

Go在堆栈中是不寻常的,因为它实际上可以在程序运行时增加堆栈的大小。这是可能的,因为每个goroutine都有自己的堆栈,并且goroutine由Go运行时而不是基础操作系统管理。这具有优点(Go堆栈开始很小,使用的内存较少)和缺点(当堆栈需要增长时,需要复制堆栈上的所有数据,这很慢)。还可以编写最坏情况下的代码,导致堆栈一遍又一遍地增长和缩小。

堆是由垃圾回收器(或手动在C和C++等语言中)管理的内存。我们不会讨论垃圾回收器算法实现细节,但它们比仅移动堆栈指针复杂得多。存储在堆上的任何数据只要可以追踪回指向堆栈上的指针类型变量,就是有效的。一旦没有更多的指针指向该数据(或指向指向该数据的数据),数据就会变成 垃圾 ,垃圾回收器的工作就是清除它。

C程序中常见的错误之一是返回指向本地变量的指针。在C中,这会导致指针指向无效内存。Go编译器更加聪明。当它看到返回指向本地变量的指针时,它会将本地变量的值存储在堆上。

总结

Go支持垃圾回收(GC),因此我们不必处理内存分配和释放。但是,GC可能会稍微减慢我们的程序。我们学习了如何在Go程序中分配内存以及如何使用runtime包读取不同类型内存的统计信息、释放内存。我们学习了如何使用runtime.ReadMemStats()函数监视程序的内存使用情况和垃圾回收器的性能。#

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论