学习与交流:Go语言技术微信群
商务合作加微信:LetsFeng
学习与分享:GoLand2022 正版激活码 全家桶通用版
课本,文档学习Go语言,个人强烈推荐这本书
现在就开始你的Go语言学习之旅吧!人生苦短,let’s Go.
Go内部内存结构
Go运行时将Goroutines(G)调度到逻辑处理器(P)上执行。每个P都有一台逻辑机器(M)。在这篇文章中,我们将使用P、M和G。
页堆page heap(mheap)
大对象(大小> 32kb的对象)直接从mheap分配。这些大对象申请请求是以获取中央锁(central lock)为代价的,因此在任何给定时间点只能满足一个P的请求。
-
mspan:mspan是mheap中管理的内存页的最基本结构。这是一个双向链接列表,其中包含起始页面的地址,span size class和span中的页面数量。像TCMalloc一样,Go将内存页按大小分为67个不同类别,大小从8字节到32KB,如下图所示
-
mcentral:mcentral将相同大小级别的span归类在一起。每个mcentral包含两个mspanList: -
empty:双向span链表,包括没有空闲对象的span或缓存mcache中的span。当此处的span被释放时,它将被移至non-empty span链表。 -
non-empty:有空闲对象的span双向链表。当从mcentral请求新的span,mcentral将从该链表中获取span并将其移入empty span链表。
-
arena:堆在已分配的虚拟内存中根据需要增长和缩小。当需要更多内存时,mheap从虚拟内存中以每块64MB(对于64位体系结构)为单位获取新内存, 这块内存被称为arena。这块内存也会被划分页并映射到span。 -
mcache:这是一个非常有趣的构造。mcache是提供给P(逻辑处理器)的高速缓存,用于存储小对象(对象大小<= 32Kb)。尽管这类似于线程堆栈,但它是堆的一部分,用于动态数据。所有类大小的mcache包含scan和noscan类型mspan。Goroutine可以从mcache没有任何锁的情况下获取内存,因为一次P只能有一个锁G。因此,这更有效。mcache从mcentral需要时请求新的span。
栈
这是栈存储区,每个Goroutine(G)有一个栈。在这里存储了静态数据,包括函数栈帧,静态结构,原生类型值和指向动态结构的指针。这与分配给每个P的mcache不是一回事。
Go内存使用(栈与堆)
现在我们已经清楚了内存的组织方式,现在让我们看看程序执行时Go是如何使用Stack和Heap的。
package main
import "fmt"
type Employee struct {
name string
salary int
sales int
bonus int
}
const BONUS_PERCENTAGE = 10
func getBonusPercentage(salary int) int {
percentage := (salary * BONUS_PERCENTAGE) / 100
return percentage
}
func findEmployeeBonus(salary, noOfSales int) int {
bonusPercentage := getBonusPercentage(salary)
bonus := bonusPercentage * noOfSales
return bonus
}
func main() {
var john = Employee{"John", 5000, 5, 0}
john.bonus = findEmployeeBonus(john.salary, john.sales)
fmt.Println(john.bonus)
}与许多垃圾回收语言相比,Go的一个主要区别是许多对象直接在程序栈上分配。Go编译器使用一种称为“逃逸分析”[8]的过程来查找其生命周期在编译时已知的对象,并将它们分配在栈上,而不是在垃圾回收的堆内存中。 在编译过程中,Go进行了逃逸分析,以确定哪些可以放入栈(静态数据),哪些需要放入堆(动态数据)。我们可以通过运行带有-gcflags '-m'标志的go build命令来查看分析的细节。对于上面的代码,它将输出如下内容: ❯ go build -gcflags '-m' gc.go
# command-line-arguments
temp/gc.go:14:6: can inline getBonusPercentage
temp/gc.go:19:6: can inline findEmployeeBonus
temp/gc.go:20:39: inlining call to getBonusPercentage
temp/gc.go:27:32: inlining call to findEmployeeBonus
temp/gc.go:27:32: inlining call to getBonusPercentage
temp/gc.go:28:13: inlining call to fmt.Println
temp/gc.go:28:18: john.bonus escapes to heap
temp/gc.go:28:13: io.Writer(os.Stdout) escapes to heap
temp/gc.go:28:13: main []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape让我们将其可视化。单击下方图片下载幻灯片,然后翻阅幻灯片,以查看上述程序是如何执行的以及如何使用栈和堆存储器的: 可视化程序执行过程中栈和堆的使用 正如你看到的:
-
main函数被保存栈中的“main栈帧”中 -
每个函数调用都作为一个栈帧块被添加到栈中 -
包括参数和返回值在内的所有静态变量都保存在函数的栈帧块内 -
无论类型如何,所有静态值都直接存储在栈中。这也适用于全局范畴 -
所有动态类型都在堆上创建,并且被栈上的指针所引用。小于32Kb的对象由P的mcache分配。这同样适用于全局范畴 -
具有静态数据的结构体保留在栈上,直到在该位置将任何动态值添加到该结构中为止。该结构被移到堆上。 -
从当前函数调用的函数被推入堆顶部 -
当函数返回时,其栈帧将从栈中删除 -
一旦主过程(main)完成,堆上的对象将不再具有来自Stack的指针的引用,并成为孤立对象
Go内存管理
内存分配
许多采用垃圾收集的编程语言都使用分代内存结构来使收集高效,同时进行压缩以减少碎片。正如我们前面所看到的,Go在这里采用了不同的方法,Go在构造内存方面有很大的不同。
Go使用线程本地缓存(thread local cache)来加速小对象分配,并维护着scan/noscan的span来加速GC。这种结构以及整个过程避免了碎片,从而在GC期间无需做紧缩处理。让我们看看这种分配是如何发生的。
Go根据对象的大小决定对象的分配过程,分为三类:
微小对象(Tiny)(size <16B):使用mcache的微小分配器分配大小小于16个字节的对象。这是高效的,并且在单个16字节块上可完成多个微小分配。
注意:您可以在此处[9]找到以幻灯片形式记录的GIF图像
垃圾收集(GC)
现在我们知道Go如何分配内存了,让我们再看看它是如何自动回收堆内存的,这对于应用程序的性能非常重要。当程序尝试在堆上分配的内存大于可用内存时,我们会遇到内存不足的错误(out of memory)。不当的堆内存管理也可能导致内存泄漏。
Go通过垃圾回收机制管理堆内存。简单来说,它释放了孤儿对象(orphan object)使用的内存,所谓孤儿对象是指那些不再被栈直接或间接(通过另一个对象中的引用)引用的对象,从而为创建新对象的分配腾出了空间。
从Go 1.12版本[10]开始,Go使用了非分代的、并发的、基于三色标记和清除的垃圾回收器。收集过程大致如下所示,由于版本之间的差异,我不想做细节的描述。
-
标记设置(mark setup, stw):GC启动时,收集器将打开写屏障(write barrier),以便可以在下一个并发阶段维护数据完整性。此步骤需要非常小的暂停(stw),因此每个正在运行的Goroutine都会暂停以启用此功能,然后继续。 -
标记(并发执行的):打开写屏障后,实际的标记过程将并行启动,这个过程将使用可用CPU能力的25%。对应的P将保留,直到该标记过程完成。这个过程是使用专用的Goroutines完成的。在这个过程中,GC标记了堆中的活动对象(被任何活动的Goroutine的栈中引用的)。当采集花费更长的时间时,该过程可以从应用程序中征用活动的Goroutine来辅助标记过程。这称为Mark Assist。 -
标记终止(stw):标记一旦完成,每个活动的Goroutine都会暂停,写入屏障将关闭,清理任务将开始执行。GC还会在此处计算下一个GC目标。完成此操作后,保留的P的会释放回应用程序。 -
清除(并发):当完成收集并尝试分配后,清除过程开始将未标记为活动的对象回收。清除的内存量与分配的内存量是同步的(即回收后的内存马上可以被再分配了)。
-
我们以一个Goroutine为例,实际过程是对所有活动Goroutine都进行的。首先打开写屏障。 -
标记过程选择GC root并将其着色为黑色,并以深度优先的树状方式遍历该该根节点里面的指针,将遇到的每个对象都标记为灰色 -
当它到达noscan span中的某个对象或某个对象不再有指针时,它完成了这个根节点的标记操作并选取下一个GC root对象 -
当扫描完所有GC root节点之后,它将选取灰色对象,并以类似方式继续遍历其指针 -
如果在打开写屏障时,指向对象的指针发生任何变化,则该对象将变为灰色,以便GC对其进行重新扫描 -
当不再有灰色对象留下时,标记过程完成,并且写屏障被关闭 -
当分配开始时(因为写屏障关闭了),清除过程也会同步进行
结论
这篇文章为您提供了Go内存结构和内存管理的概述。这里不是全面详尽的说明,有许多更高级的概念,实现细节在各个版本之间都在不断变化。但是对于大多数Go开发人员来说,这些信息就已经足够了,我希望它能帮助您编写出更好的、性能更高的应用程序,牢记这些,将有助于您避免下一个内存泄漏问题。
参考链接:https://juejin.cn/post/7107533102083211301
更多相关Go语言的技术文章或视频教程,请关注本公众号获取并查看,感谢你的支持与信任!
文章评论