团队介绍
我们是光大科技有限公司智能云计算部云计算团队容器云项目组,致力于容器技术在金融行业的落地与实践,以容器云助力金融行业科技转型。我们的团队拥有经验丰富的容器与云原生领域研发工程师和专家,将不定期分享容器和云原生行业的原创技术文章和实践经验,共同探索与见证云原生时代金融领域的前沿技术和发展趋势。
#01
概述
内存(Random Access Memory,RAM),也叫主存,是与CPU直接交换数据的内部存储器。 内存用来加载各式各样的程序与数据以供CPU直接运行与运用,是一种程序运行的重要资源。
#02
基础管理
分配:
初始化阶段会直接分配一块大内存区域,大内存区域又被切分成各个大小等级的块,放入不同的空闲list中,对象分配空间时从空闲list中取出大小合适的内存块。
回收:
内存回收时,会把不用的内存重放回空闲list。空闲内存会按照一定策略合并,以减少碎片。
#03
对象大小
在Go语言中,根据对象的大小将对象分成微对象、小对象和大对象三种。
类别 |
大小 |
微对象 |
0-16字节 |
小对象 |
16-32K字节 |
大对象 |
32K-无穷大字节 |
运行时,申请的对象大小影响Go语言运行时分配内存的过程和开销,内存分配器会基于对象的大小选择不同的处理逻辑。因为大多数情况下对象的大小都在32KB以下,所以分别处理大对象和小对象有利于提高内存分配器的性能。
#04
多级缓存
内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc和Go运行时,分配器都会引入以下三个组件分级管理内存。
-
Thread Cache(线程缓存)
-
Central Cache(中心缓存)
-
Page Heap(页堆)
线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求,因为不涉及多线程,所以也不需要使用互斥锁来保护内存,能够减少锁竞争带来的性能损耗。
当线程缓存不能满足需求时,会使用中心缓存作为补充解决小对象的内存分配。
在遇到32KB以上的大对象时,内存分配器会选择页堆直接分配大内存。
这种多层级的内存分配设计与计算机操作系统中的多级缓存有些类似。因为多数的对象都是小对象,我们可以通过线程缓存和中心缓存提供足够的内存空间,发现资源不足时从上一级组件中获取更多的内存资源。
#05
虚拟内存布局
在Go1.11版本之后,使用了稀疏的堆内存空间替代了连续的内存,解决了连续内存带来的限制以及在特殊场景下可能出现的问题。
#06
地址空间
因为所有的内存最终都是要从操作系统中申请的,所以 Go 语言在运行时构建了操作系统的内存管理抽象层,该抽象层将运行时管理的地址空间分成以下四种状态。
状态 |
解释 |
None |
内存没有被保存或者映射,是地址空间的默认状态 |
Reserved |
运行时持有该地址空间,但是访问该内存会导致错误 |
Prepared |
内存被保留,一般没有对应的物理内存访问,该片内存的行为是未定义的,可以快速转到Ready状态 |
Ready |
可以被安全访问 |
#07
垃圾回收
Go 的通过三色标记清扫法与写屏障来提高垃圾回收(GC)性能,减少 STW 的时间。
图片来源于网络
三色标记法(白、灰、黑)的流程如下:
-
所有对象最开始都是白色;
-
从 RootSet根节点集合开始找到所有可达对象,标记为灰色,放入待处理队列;
-
遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色;
-
循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象。
清理操作:
需要清除的对象也是业务不需要的对象,所以清除操作可以和业务并发。
标记操作:
如何也能与业务并发呢,Go引入了写屏障技术来解决这个问题。
如果减少STW时间,甚至不需要STW就会出现:程序正常运行需要的对象可能会被误删。
在三色标记法中,已证明的,只要保证以下两条约束其中一条达成,就可以保证对象不会被误删。
弱三色不变式
黑色对象可以引用白色对象,白色对象存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。
强三色不变式
强制性的不允许黑色对象引用白色对象。
写屏障技术就是为了维护这两条约束的。
写屏障:
当标记和程序是并发执行的,这就会造成一个问题,在标记过程中,有新的引用产生,可能会导致误清扫。清扫开始前,标记为黑色的对象引用了一个新申请的对象,它肯定是白色的,而黑色对象不会被再次扫描,那么这个白色对象无法被扫描变成灰色、黑色,它就会最终被清扫,而实际它不应该被清扫。这就需要用到屏障技术,Go 采用了写屏障,作用就是为了避免这类误清扫问题。写屏障即在内存写操作前,维护一个约束,从而确保清扫开始前,黑色的对象不能引用白色对象。
#08
插入写屏障(Go1.5版本引入)
插入屏障拦截“将白色指针插入黑色对象”的操作,标记其对应对象为灰色状态,这样就不存在黑色对象引用白色对象的情况了,满足强三色不变式。
图片来源于网络
插入屏障是一个很耗费性能的行为,而栈需要更高的性能要求,因此,插入屏障技术只运用在堆内存空间里,不会运用到栈里。
图片来源于网络
所以,上图中“对象9”就不会被插入写屏障技术标记为灰色,后续被清理掉,导致程序出错。如何处理呢?
清理操作发生前,会重新遍历扫描一次栈空间,这时需要STW保证栈中没有任何写操作。具体步骤如下:
-
把栈对象全部标记为白色
-
执行三色标记法,清除白色对象
-
结束
插入写屏障结合三色标记法,可以在回收堆对象时不使用STW,只是在栈中使用,但栈的对象数目少,STW的时间也不会太长。提高了GC性能。
#09
删除写屏障
删除屏障拦截“删除白色和灰色对象”操作,因为它是写入一个空对象。具体的操作是,被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。满足了弱三色不变式原则,保护灰色对象到白色对象的可达路径不会断。
删除写屏障需要在GC开始时STW扫描堆和栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,但结束时无需STW。
图片来源于网络
图片来源于网络
如上图所示,当删除 “对象1” 指向 “白色对象5”的关系时,将“对象5”标记为灰色。本轮GC,“对象5”以及它引用对象均不会被清理。那么对象5什么时候被删除呢?答案是等到下一轮GC,对象1肯定到达不了对象5,同时也没有从根节点出发的对象到达对象5,那么对象5就会被合法地删除。
我们可以看到删除写屏障的不足:回收精度低,一个对象即使被删除了最后一个指向它的指针,也依然可以活一轮GC,在下一轮GC中才被清除。
#10
混合写屏障(Go1.8版本引入)
Go中的混合写屏障满足的是变形的弱三色不变式,同样允许黑色对象引用白色对象,白色对象处于灰色保护状态,但是只由堆上的灰色对象保护。
结合了Yuasa的删除写屏障和Dijkstra的插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
-
混合写屏障继承了插入写屏障的优点,起始无需 STW 打快照,直接并发扫描垃圾即可;
-
混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,扫描过一次就不需要扫描了,这样就消除了插入写屏障时期最后 STW 的重新扫描栈;
-
混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的是 GC 过程全程无 STW;
-
混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还是要停止这个 goroutine 赋值器的工作的哈(针对一个 goroutine 栈来说,是暂停扫的,要么全灰,要么全黑,原子状态切换)。
GC 触发条件
-
当前内存分配达到一定比例则触发
-
两分钟没有触发过 GC ,则触发 GC
-
手动触发,调用 runtime.GC()
作为Go开发者,了解Go的内存管理和垃圾回收,会让我们在程序运行时对runtime多一分信任。在认识了内存的有效分配、低效分配和有害分配之后,我们对代码的组织将会更加健壮。本文简单描述了Go垃圾回收的一些实现方案,但要注意的是,随着版本的更新迭代,回收算法的实现细节也一直在发展变化,我们要关注的是其中的理念和思路。希望对大家有所帮助,谢谢。
参考文档
References
-
https://studygolang.com/articles/21569
-
https://www.jb51.net/article/157744.htm
-
https://www.bookstack.cn/read/qcrao-Go-Questions/spilt.9.GC-GC.md
留言区
MESSAGE
浅谈Go语言内存管理 留言区
往期 / 回顾
-
设计模式在项目实战中的应用
-
浅谈数据安全
-
不用数据库非好汉,不懂索引也遗憾
-
开源云编排引擎Cloudify功能介绍
欢迎关注EBCloud!
作者 | 佘彬彬
原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/33203.html