go语言gc机制 go语言 gin

Go GC 简介 GC 与 mutator 线程并发运行,允许多个 GC 线程并行运行
GC 是一个使用写屏障的并发标记和清除 。
【go语言gc机制 go语言 gin】 GC 是非分代的,非紧凑的 。
Allocation 是按照大小隔离每个 P 分配的区域来完成的,以在消除常见情况下的锁的同时,最小化碎片 。
了解 GC 的好地方,可以从 Richard Jones 的 gchandbook.org 开始 。
1. GC 执行清除终止
a.Stop the world,这将导致所有 P 达到 GC 安全点 。
b.清除任何未清除过的 spans , 只有在预期时间之前强制执行此 GC 周期时 , 才会有未清除的 span。
2. GC 执行标记阶段
a.准备标记阶段 , 将 gcphase 设置为 _GCmark (从 _GCoff 开始),启用写屏障,启用 mutator assist,并对根标记作业进行排队 。
在所有P都启用写屏障之前,不会扫描任何对象,这是使用STW完成的 。
b.Start the world ,从现在开始,GC 工作由调度器启动的 标记worker 和作 为allocation的一部分执行的assists来完成 。
写屏障将覆写的指针和任何指针写的新指针值都着色 。
新分配的对象立即被标记为黑色 。
c.GC 执行根标记作业 。包括: 扫描所有栈 , 着色所有全局变量,以及 着色堆外运行时数据结构中的任何堆指针。
扫描栈会停止goroutine , 对goroutine栈中找到的任何指针进行着色,然后恢复goroutine 。
d.GC 耗尽灰色对象的工作队列,将每个 灰色 对象扫描为 黑色,并对在该对象中找到的所有指针进行着色(反过来可能会将这些指针添加到工作队列中) 。
e.由于 GC work 分散在本地缓存中 , 因此 GC 使用 分布式终止算法 来检测何时不再有根标记作业或灰色对象(参见 gcMarkDone 函数) 。
此时,GC 状态转换到标记终止( gcMarkTermination ) 。
3. GC 执行标记终止gcMarkTermination
a.Stop the world
b.将 gcphase 设置为 _GCmarktermination ,并禁用 workers 和 assists 。
c.进行内务整理,如 flushing mcaches
4. GC 执行清除阶段
a.准备清除阶段,将 gcphase 设置为 _GCoff,设置清除状态并禁用写屏障 。
b.Start the world,从现在开始,新分配的对象是白色的,如有必要,在使用 spans 前 allocating 清除 spans。
c.GC 在后台进行 并发清除 并响应 allocation,见下面的描述 。
5. 当分配足够时,重复上面 1 开始的步骤,参见下面关于GC rate 的讨论 。
清除阶段与正常程序执行并发进行 。
在后台 goroutine 中,堆被惰性(当 goroutine 需要另一个 span 时)且并发地逐个 span 扫描(这有助于不是 CPU bound 的程序) 。
在 STW 标记终止 的结尾,所有的 span 都被标记为 需要清除。
后台清除器 goroutine 简单地逐个清除 span。
为了避免在存在未清除的 span 时请求更多的 OS内存,当 goroutine 需要另一个 span 时,它首先尝试通过清除来回收这些内存 。
当 goroutine 需要分配一个新的 小对象span 时,它会清除相同大小的小对象 span ,直到释放至少一个对象为止 。
当 goroutine 需要从堆中分配 大对象span 时,它会清除 span , 直到将至少那么多页面释放到堆中 。
有一种情况,这可能是不够的:如果 goroutine 清除并释放两个不相邻的 单页span 到堆中,那么它将分配一个新的 双页span,但是仍然可以有其他 单页未清除的span,可以组合成 双页的span。
确保在未清除的 span 上不进行任何操作(这会破坏 GC 位图中的标记位)至关重要 。
在 GC 期间,所有 mcache 都被刷新到 中央缓存 中 , 因此它们是空的 。
当一个 goroutine 抓取一个新的 span 到 mcache 时 , goroutine 会清除 mcache。
当 goroutine 显式释放对象或设置 finalizer 时,goroutine 确保 span 已经清除(通过清除或者等待并发清除完成) 。
finalizer goroutine 仅在所有 span 已经清除时才开始 。
当下一次 GC 启动时,它将清除所有尚未清除的 span (如果有的话) 。
下一次 GC 是在我们分配了与已经使用的内存成正比的额外内存量之后 。
该比例由 GOGC 环境变量控制(默认为 100 ) 。
如果 GOGC=100,而我们使用的是 4M ,那么当达到 8M 时,我们将再次进行 GC(此标记在 next_gc 变量中被跟踪) 。
获取 GOGC :
这使得 GC成本 与 allocation 成本 成线性比例 。
调整 GOGC 只会改变线性常量(以及使用的额外内存量) 。
为了防止在扫描大型对象时出现长时间的暂停,并提高并行性,垃圾收集器将大于 maxObletBytes 的对象的扫描作业分解为最多 maxObletBytes 的 oblets。
当扫描遇到大对象时,它只扫描第一个 oblet ,并将其余 oblets 作为新的扫描作业排队 。
为什么go语言适合开发网游服务器端个人觉得golang十分适合进行网游服务器端开发 , 写下这篇文章总结一下 。从网游的角度看:要成功的运营一款网游 , 很大程度上依赖于玩家自发形成的社区 。只有玩家自发形成一个稳定的生态系统,游戏才能持续下去,避免鬼城的出现 。而这就需要多次大量导入用户 , 在同时在线用户量达到某个临界点的时候,才有可能完成 。因此,多人同时在线十分有必要 。再来看网游的常见玩法,除了排行榜这类统计和数据汇总的功能外 , 基本没有需要大量CPU时间的应用 。以前的项目里,即时战斗产生的各种伤害计算对CPU的消耗也不大 。玩家要完成一次操作,需要通过客户端-服务器端-客户端这样一个来回 , 为了获得高响应速度,满足玩家体验,服务器端的处理也不能占用太多时间 。所以 , 每次请求对应的CPU占用是比较小的 。网游的IO主要分两个方面,一个是网络IO,一个是磁盘IO 。网络IO方面,可以分成美术资源的IO和游戏逻辑指令的IO,这里主要分析游戏逻辑的IO 。游戏逻辑的IO跟CPU占用的情况相似,每次请求的字节数很小 , 但由于多人同时在线,因此并发数相当高 。另外,地图信息的广播也会带来比较频繁的网络通信 。磁盘IO方面,主要是游戏数据的保存 。采用不同的数据库,会有比较大的区别 。以前的项目里,就经历了从MySQL转向MongoDB这种内存数据库的过程,磁盘IO不再是瓶颈 。总体来说,还是用内存做一级缓冲,避免大量小数据块读写的方案 。针对网游的这些特点,golang的语言特性十分适合开发游戏服务器端 。首先,go语言提供goroutine机制作为原生的并发机制 。每个goroutine所需的内存很少,实际应用中可以启动大量的goroutine对并发连接进行响应 。goroutine与gevent中的greenlet很相像 , 遇到IO阻塞的时候,调度器就会自动切换到另一个goroutine执行,保证CPU不会因为IO而发生等待 。而goroutine与gevent相比,没有了python底层的GIL限制,就不需要利用多进程来榨取多核机器的性能了 。通过设置最大线程数,可以控制go所启动的线程,每个线程执行一个goroutine,让CPU满负载运行 。同时,go语言为goroutine提供了独到的通信机制channel 。channel发生读写的时候 , 也会挂起当前操作channel的goroutine,是一种同步阻塞通信 。这样既达到了通信的目的 , 又实现同步 , 用CSP模型的观点看 , 并发模型就是通过一组进程和进程间的事件触发解决任务的 。虽然说,主流的编程语言之间,只要是图灵完备的,他们就都能实现相同的功能 。但go语言提供的这种协程间通信机制,十分优雅地揭示了协程通信的本质,避免了以往锁的显式使用带给程序员的心理负担,确是一大优势 。进行网游开发的程序员 , 可以将游戏逻辑按照单线程阻塞式的写,不需要额外考虑线程调度的问题,以及线程间数据依赖的问题 。因为,线程间的channel通信,已经表达了线程间的数据依赖关系了,而go的调度器会给予妥善的处理 。另外,go语言提供的gc机制,以及对指针的保护式使用 , 可以大大减轻程序员的开发压力 , 提高开发效率 。展望未来 , 我期待go语言社区能够提供更多的goroutine间的隔离机制 。个人十分推崇erlang社区的脆崩哲学 , 推动应用发生预期外行为时,尽早崩溃,再fork出新进程处理新的请求 。对于协程机制 , 需要由程序员保证执行的函数不会发生死循环,导致线程卡死 。
Golang什么时候会触发GCGolang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个回收动作呢?
源码主要位于文件src/runtime/mgc.gogo version 1.16
触发条件从大方面说 , 可分为 手动触发 和 系统触发 两种方式 。手动触发一般很少用,主要由开发者通过调用runtime.GC()函数来实现,而对于系统自动触发是运行时根据一些条件判断来进行的,这也正是本文要介绍的内容 。
不管哪种触发方式,底层回收机制是一样的 , 所以我们先看一下手动触发,根据它来找系统触发的条件 。
可以看到开始执行GC的是gcStart()函数,它有一个gcTrigger参数,是一个触发条件结构体,它的结构体也很简单 。
其实在Golang 内部所有的GC都是通过gcStart()函数 , 然后指定一个 gcTrigger的参数来开始的,而手动触发指定的条件值为gcTriggerCycle。gcStart 是一个很复杂的函数 , 有兴趣的可以看一下源码实现 。
对于kind的值有三种,分别为 gcTriggerHeap 、gcTriggerTime和gcTriggerCycle。
运行时会通过gcTrigger.test()函数来决定是否需要触发GC,只要满足上面基中一个即可 。
到此我们基本明白了这三种触发GC的条件 , 那么对于系统自动触发这种,Golang 从一个程序的开始到运行 , 它又是如何一步一步监控到这个条件的呢?
其实 runtime 在程序启动时,会在一个初始化函数init()里启用一个forcegchelper()函数,这个函数位于proc.go文件 。
为了减少系统资源占用 , 在forcegchelper函数里会通过goparkunlock()函数主动让自己陷入休眠,以后由sysmon()监控线程根据条件来恢复这个gc goroutine 。
可以看到sysmon()会在一个for语句里一直判断这个 gcTriggerTime这个条件是否满足 , 如果满足的话,会将forcegc.g这个 goroutine 添加到全局队列里进行调度(这里forcegc是一个全局变量) 。
调度器在调度循环runtime.schedule中还可以通过垃圾收集控制器的runtime.gcControllerState.findRunnabledGCWorker获取并执行用于后台标记的任务 。
【golang】内存逃逸常见情况和避免方式因为如果变量的内存发生逃逸 , 它的生命周期就是不可知的,其会被分配到堆上 , 而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,go实现了gc垃圾回收机制 , 但gc会影响程序运行性能,所以要尽量减少程序的gc操作 。
1、在方法内把局部变量指针返回 , 被外部引用,其生命周期大于栈,则溢出 。
2、发送指针或带有指针的值到channel , 因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放 。
3、在一个切片上存储指针或带指针的值 。比如[]*string,导致切片内容逃逸,其引用值一直在堆上 。
4、因为切片的append导致超出容量,切片重新分配地址 , 切片背后的存储基于运行时的数据进行扩充,就会在堆上分配 。
5、在interface类型上调用方法 , 在Interface调用方法是动态调度的,只有在运行时才知道 。
1、go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口 。
2、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大 。
3、预先设定好slice长度,避免频繁超出容量,重新分配 。
go语言gc机制的介绍就聊到这里吧,感谢你花时间阅读本站内容 , 更多关于go语言 gin、go语言gc机制的信息别忘了在本站进行查找喔 。

    推荐阅读