Go Gc

GC 版本迭代 v1.1 STW v1.3 Mark STW v1.5 三色标记 v1.8 混合写屏障 三色标记 不变性条件,写屏障 如何标记,gcmarkerbits,0表示白色对象,1表示灰色或黑色对象。p中wbBuf和gcw以及全局workbuf来表示标记队列 对象元信息,查找到对象分配内存的span gc触发时机 分配内存时,当前已分配内存与上次内存比较 sysmon每2min检查执行一次 调用runtime.gc强制执行 三色标记 传统的标记-清除需要长时间STW以完成标记和清扫的过程,三色标记用于改进减小STW的时间。 三色标记中将对象分为三种类型: 白色:可能存活的对象,在初始阶段所有对象为白色,在标记完成后,所有的白色对象视为垃圾 灰色:确认存活的对象,但其引用了白色对象,因此要对灰色对象进行递归扫描 黑色:确认存活的对象,扫描完成的对象,根对象可达 三色标记过程: 初始时所有对象为白色 将根对象标记为灰色,根对象包括运行栈中的对象以及全局对象 对所有灰色对象进行扫描,将灰色对象引用的对象标记为灰色,并将该灰色对象标记为黑色 重复上述过程,直到没有灰色对象 标记结束后,程序中只有黑色对象和白色对象,黑色对象为确认存活的对象,白色对象为垃圾 三色不变性 当三色标记的标记过程是STW时,可以确保标记过程的正确性,但STW要消耗大量时间。但如果将三色标记的标记过程和用户代码并发执行,则可能出现对象丢失. 三色标记正确性被破坏,如下图,B对象本不该回收的对象,由于引用的改变,导致其被回收了。 图来自https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/barrier/ 使得三色标记正确性被破坏的两个条件: 黑色对象引用了白色对象 灰色对象达到白色对象的未访问过的路径被破坏,即黑色对象引用的白色对象,但灰色对象无法找到一条路径到达该白色对象 当上述两个条件同时满足时,三色标记就可能丢失对象。 想要使得三色标记正确,就必须破坏上述条件中的任意一个条件,因此三色标记不变式: 强三色不变式:黑色对象不能指向白色对象,只能指向灰色或黑色对象。该不变式破坏了两个条件。 弱三色不变式:黑色对象执行的白色对象,必须存在一条从灰色对象经过零个或多个白色对象可达该白色对象的路径。 屏障 Dijkstra插入写屏障 当某一对象的引用被插入到已经标记为黑色的对象中,需要将其标记为灰色对象。 将有存活可能的对象标记为灰色,以满足强三色不变式。 // Dijkstra 插入屏障 // slot表示旧指向的对象,ptr表示新指向的对象 func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) { // 将新指向的对象标记为灰色 shade(ptr) *slot = ptr } 特点: 可能产生部分黑色对象的垃圾,需要在下一次GC中回收 对于栈上的对象,使用插入写屏障后耗费大量性能,因此不在栈上开启插入写屏障。 由于在栈上不开启插入写屏障,因此当栈上的黑色对象指向了白色的对象时,因为没有屏障,因此白色对象会被错误回收。因此插入写屏障在标记结束后,会STW并再次重新扫描栈。 Yuasa删除写屏障 起始时,STW将整个栈的可达对象标记为黑色,将所有可达对象在灰色保护下...

March 15, 2021 · 2 min · Theme PaperMod

Go Memory

TCMalloc 给每个线程都分配一个局部缓存,采用线程局部数据技术。小块内存的分配从线程局部缓存分配即可满足。同时周期性地将线程缓存内存回收到中心缓存中,线程局部缓存不足时从中心缓存中申请。 小对象分配(<=256K) 对于256K以内的内存大小,划分了约88个种类,每个size class对应一个种类,分配时是将申请内存大小向上取整到size class类别的大小,比如申请5bytes会分配8bytes,由于分配的内存会大于申请的内存,则会造成内存浪费。 每个线程分配单独的缓存ThreadCache,ThreadCache中对于每个size class都分配一个FreeList用于分配和回收该大小的内存空间。小对象的分配直接在ThreadCache中对应大小的FreeList中获取空闲对象 全局共享缓存CentralCache,对于每个size class都分配一个CentralFreeList,当ThreadCache尝试分配发现没有空闲对象时,就从CentralCache的对应大小的CentralFreeList中获取一部分空闲对象,这里需要加锁。 当CentralCache中的CentralFreeList没有空闲对象时,向PageHeap申请内存,将其划分成对应大小的空闲对象,加入到空闲链表中。 内存回收时,将空闲对象插入对应的ThreadCache的FreeList中,当满足一定条件将FreeList中的内存归还给CentralCache,当满足一定条件将CentralCache的CentralFreeList中的内存归还给PageHeap 中对象分配(256K<size<=1M) 对于中对象的分配,从PageHeap中的FreeLsit中获取,PageHeap划分为128个span,每个span由1,2…128个page组成。 分配内存时首先向上取整到对应K个page 从PageHeap的对应大小的FreeList中尝试获取span 如果该大小没有空闲的span,则增大page往下寻找 找个n个page的span后,将该span划分为两部分 将k个page的span作为结果返回 将剩余的n-k个page的span,插入到PageHeap中 如果128个spanlist都没有空闲对象,则当做大对象来分配 大对象分配(>1M) 对于大于1m的对象或者小于1m但PageHeap的spanList无法满足的对象,则采用大对象分配。 首先向上取整到对应K个page 大于128K的span都是使用红黑树来保存的,然后使用best-fit首次适应算法,找到合适的span 找个n个page的span后,将该span划分为两部分 将k个page的span作为结果返回 将剩余的n-k个page的span,如果n-k>128则插入红黑树中继续用于大对象的分配,n-k<=128则插入到PageHeap中 如果无法找到足够大小的空闲对象,则需要向系统申请新的内存 内存分配 分级分配 golang根据对象内存大小划分为3类, 微对象:0~16B 小对象:16B~32KB,以及<16B的指针对象 大对象:32KB以上 三级内存管理 线程缓存:每个线程有自己的内存缓存,如果内存大小能够满足,则直接在线程缓存上分配,不需要考虑全局锁的问题 中心缓存:当线程缓存无法满足时,就需要在全局的中心缓存来分配 页堆:当需要分配32KB以上的大对象时,使用页堆 内存管理 内存管理单元mspan 每个mspan都管理npages个8K的页 比如最小的分配8B的mspan,则需要一个8K的页,因此最后可以分配8K/8B=1024个对象 比如最大的分配32K的mspan,则需要4个8K的页,因此最多可以分配32K/32K=1个对象 mspan中使用allocBits来标记内存的占用情况,1表示已分配,0表示未使用 mspan分为无指针的noscan和有指针的两种,与gc有关 线程缓存mcache 每个P都有自己的mcache,用于分配小对象 每个mcache都有alloc [numSpanClasses]*mspan,67*2=134个mspan,用来分配不同大小的对象 每个P在初始化时会调用allocmcache来初始化mcache 初始化时mcache中每个mspan都是空的emptymspan,并没有内存空间 当mcache中的mspan满或者为emptymspan时,调用refill从中心缓存中获取至少包含一个空闲对象空间的mspan 中心缓存mcentral 每个spanclass对应一个mcentral,共134个 mcentral中主要维护两个spanlist nonempty mSpanList:表示存在空闲对象空间的mspan empty mSpanList:表示没有空闲对象空间的mspan或者缓存在mcache中 主要方法cacheSpan该方法来返回span给mcache // Allocate a span to use in an mcache. func (c *mcentral) cacheSpan() *mspan { sg := mheap_....

March 9, 2021 · 2 min · Theme PaperMod

Go Stack

go 栈 操作系统进程内存 虚拟内存空间大小 Linux为每个进程维护了一个单独的虚拟地址空间。在32位系统上,该虚拟地址空间大小为2^32Bytes = 4G,其中内核空间1G,在高地址处,用户空间3G,在低地址处;在64位系统上,该虚拟地址空间大小为2^48Bytes = 256TB,其中用户和内核各128TB,用户空间0x0000000000000000~0x00007fffffffffff,内核空间0xffff800000000000~0xffffffffffffffff,中间16M TB暂未用到。 linux查看cpu可访问地址空间位数 cat /proc/cpuinfo | grep address address sizes : 43 bits physical, 48 bits virtual 48 bits virtual表示可访问地址空间位数为48,即256TB。 进程的虚拟内存空间分布 高地址+---------> +------------------+-----------+ | | | | 进程相关数据结构 | | | | | +------------------+ v | | | 物理内存 | 内核区 | | +------------------+ ^ | | | | 内核代码和数据 | | | | | +------------------------------+ |XXXXXXXXXXXXXXXXXX| +------------------------------+ | | | | 用户栈 | | | | | %rsp +--------> +--------+---------+ | | | | | +--------v---------+ | | | | |共享区内存映射区域| | | | | +--------^---------+ | | | | | brk +--------> +--------+---------+ v | | | 运行时堆 | 用户区 | | +------------------+ ^ | | | | 未初始化数据 | | | | | +------------------+ | | | | | 已初始化数据 | | | | | +------------------+ | | | | | 代码 | | | | | +------------------+ | |XXXXXXXXXXXXXXXXXX| | 低地址+---------> +------------------------------> 64位代码段总是从地址0x400000开始的。...

January 26, 2021 · 3 min · Theme PaperMod

Go Func Stack

go函数调用过程分析 对go的简单函数调用过程通过GDB调试的方式分析一下,查看一下函数的栈帧变化情况。 源代码 源代码: package main func add(a, b int) (int, int) { c := 3 c = c + 1 return a + b, b - a } func main() { add(1, 2) } // GOOS=linux GOARCH=amd64 go build -gcflags=all="-N -l" a.go gdb调试开始 进入gdb调试 通过gdb a进入gdb调试 gdb a (gdb) info files Symbols from "/home/qraffa/gopkg/a". Local exec file: `/home/qraffa/gopkg/a', file type elf64-x86-64. Entry point: 0x465860 0x0000000000401000 - 0x0000000000467907 is .text 0x0000000000468000 - 0x00000000004884a6 is ....

January 26, 2021 · 3 min · Theme PaperMod

Go-GMP

goroutine调度 线程实现模型 用户级线程(M:1) 将多个线程放在用户空间中,对应一个系统线程,线程之间由用户空间来进行调度,因此不需要陷入内核状态。但由于内核线程无法感知用户线程,一旦一个用户线程因为某些原因阻塞,则整个进程就阻塞,即使其他线程是可以运行的。由于没有时间中断,因此一个线程开始执行,那么其他线程将无法执行,除非运行的线程主动让出cpu进行新的调度。 内核级线程(1:1) 将一个用户线程对应一个内核线程,线程由内核进行调度。当用户进程的一个线程阻塞时,内核可以调度其他线程来运行。但由于一个用户线程对应一个内核线程,因此线程之间的调度需要进入内核状态,并且用户线程过多的启动新线程,会影响系统对线程的调度性能。linux使用该线程模型。 混合线程(M:N) 将用户线程与内核线程多路复用,一个进程与多个内核线程关联,多个用户线程可以运行在一个内核线程上。内核只需要调度内核线程,用户线程之间的调度由用户空间完成。 x:y表示x个用户线程对应y个内核线程 GMP调度模型 G表示goroutine,M表示machine,P表示processor G G的结构体定义在runtime2.go#L406 type g struct { stack stack // 表示该g的栈内存 m *m // 指向当前m sched gobuf // 调度上下文,用于协程调度上下文的切换 startpc uintptr // 指向groutine的function atomicstatus uint32 // 表示该g的状态 } // 栈内存地址[lo,hi) type stack struct { lo uintptr hi uintptr } G的几个状态 状态 _Gidle G已经被分配,但未被初始化 _Grunnable G已经在执行队列中等待执行 _Grunning G可以执行用户代码,拥有了栈,并且分配了M和P _Gsyscall G在执行系统调用,未在执行用户代码分配了M _Gwaiting G阻塞,未执行用户代码,未处在等待队列中 _Gdead G不再被使用,可能由一个G刚刚退出,或者在空闲队列中,或者是仅仅被初始化 _Gcopystack G正在发生栈复制,未执行用户代码,未处在等待队列中 _Gpreempted G被抢占,需要通过CAS将状态变为_Gwaiting等待唤醒》 每个goroutine都维护着一个自己的栈区,初始大小为2KB,当goroutine运行时,栈空间不足则会触发morestack,因此就会初始化一块两倍大小的栈空间,然后进行栈复制过程,所以这里会将G状态改为_Gcopystack M M对应操作系统中线程,最多只有GOMAXPROCS个线程在活跃执行。...

January 20, 2021 · 13 min · Theme PaperMod