Golang原理剖析(彻底理解Go语言栈内存/堆内存、Go内存管理)

文章目录

计算机为什么需要内存?

计算机是运行自动化程序的载体,程序(或称之为进程)由可执行代码被执行后产生。那么计算机在运行程序的过程中为什么需要「内存」呢?为了轻松理解这个问题,我们先来简单看看:

  • 代码的本质
  • 可执行代码被执行后,程序的运行过程

代码的本质

简单来看代码主要包含两部分:

  • 指令部分:中央处理器CPU可执行的指令
  • 数据部分:常量等

代码包含了指令,代码被转化为可执行二进制文件,被执行后加载到内存中,中央处理器CPU通过内存获取指令,图示如下:

程序的运行过程

可执行代码文件被执行之后,代码中的待执行指令被加载到了内存当中。这时CPU就可以从内存中获取指令、并执行指令。

CPU执行指令简易过程分为三步:

  • 取指:CPU控制单元从内存中获取指令
  • 译指:CPU控制单元解析从内存中获取指令
  • 执行:CPU运算单元负责执行具体的指令操作

我们通过一个简易的时序图来看看CPU获取并执行指令的过程:

内存的作用

通过以上我们可以基本看出「内存」在计算机中扮演的角色:

  • 暂存二进制可执行代码文件中的指令、预置数据(常量)等
  • 暂存指令执行过程中的中间数据
  • 等等

至此我们基本明白了内存存在的意义。但是呢,我们又经常会听到关于「栈内存」、「堆内存」的概念,那「栈内存」和「堆内存」到底是什么呢?接下来我们继续来看看这个问题。

为什么需要栈内存?

程序在使用内存的过程中,不仅仅只需要关注内存的分配问题,还需要关注到内存使用完毕的回收问题,这就是内存管理中面临的最大两个问题:

  • 内存的分配
  • 内存的回收

有没有简单、高效、且通用的办法统一解决这个内存分配问题呢?

答:最简单、高效地分配和回收方式就是对一段连续内存的「线性分配」,「栈内存」的分配就采用了这种方式

「栈内存」的简易管理过程:

1. 栈内存分配逻辑:current - alloc

2. 栈内存释放逻辑:current + release

通过利用「栈内存」,CPU在执行指令过程中可以高效的存储临时变量。其次:

  • 栈内存的分配过程:看起来像不像数据结构「栈」的入栈过程。
  • 栈内存的释放过程:看起来像不像数据结构「栈」的出栈过程。

所以同时你应该也理解了「为什么称之为栈内存?」。「栈内存」是计算机对连续内存的采取的「线性分配」管理方式,便于高效存储指令运行过程中的临时变量。

为什么需要堆内存?

假如函数A内变量是个指针且被函数B外的代码依赖,如果对应变量内存被回收,这个指针就成了野指针不安全。怎么解决这个问题呢?

答:这就是「堆内存」存在的意义,Go语言会在代码编译期间通过「逃逸分析」把分配在「栈」上的变量分配到「堆」上去。

「堆内存」如何回收呢?

答:堆内存通过「垃圾回收器」回收

Go语言分配的是虚拟内存

通过以上我们了解了「内存」、「栈内存」、「堆内存」存在的意义。除此之外,还有一个重要的知识点:程序和操作系统实际操作的都是虚拟内存,最终由CPU通过内存管理单元MMU(Memory Manage Unit)把虚拟内存的地址转化为实际的物理内存地址。图示如下:

使用虚拟内存的原因:

  • 对于我们的进程而言,可使用的内存是连续的
  • 安全,防止了进程直接对物理内存的操作(如果进程可以直接操作物理内存,那么存在某个进程篡改其他进程数据的可能)
  • 提升物理内存的利用率,当进程真正要使用物理内存时再分配
  • 虚拟内存和物理内存是通过MMU(管理单元内存Memory Management Unit)映射的

所以,一个很重要的知识点:

结论:Go语言源代码对「栈内存」和「堆内存」的分配、释放等操作,都是对虚拟内存的操作,最终中央处理器CPU会统一通过MMU(管理单元内存Memory Management Unit)转化为实际的物理内存

也就是说Go语言源代码中:

  • 「栈内存」的分配或释放都是对虚拟内存的操作
  • 「堆内存」的分配或释放都是对虚拟内存的操作

接着我们分别通过分配时机、分配过程两部分,来看看Go语言栈内存和堆内存的分配。

Go语言栈内存的分配

Go语言栈内存分配的时机

栈内存分配时机-创建Goroutinue时

创建g0函数代码片段:

go 复制代码
// src/runtime/proc.go::1720
// 创建 m
func allocm(_p_ *p, fn func(), id int64) *m {
    // ...略
    if iscgo || mStackIsSystemAllocated() {
		mp.g0 = malg(-1)
	} else {
        // 创建g0 并申请8KB栈内存
        // 依赖的malg函数
		mp.g0 = malg(8192 * sys.StackGuardMultiplier)
	}
    // ...略
}

创建g函数代码片段:

go 复制代码
// src/runtime/proc.go::3999
// 创建一个带有任务fn的goroutine
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
    // ...略
    newg := gfget(_p_)
    if newg == nil {
      // 全局队列、本地队列找不到g 则 创建一个全新的goroutine
      // _StackMin = 2048
      // 申请2KB栈内存
      // 依赖的malg函数
      newg = malg(_StackMin)
      casgstatus(newg, _Gidle, _Gdead)
      allgadd(newg)
    }
    // ...略
}

以上都依赖malg函数代码片段,其作用是创建一个全新g:

go 复制代码
// src/runtime/proc.go::3943
// 创建一个指定栈内存的g
func malg(stacksize int32) *g {
	newg := new(g)
	if stacksize >= 0 {
		// ...略
		systemstack(func() {
            // 分配栈内存
			newg.stack = stackalloc(uint32(stacksize))
		})
		// ...略
	}
	return newg
}

栈内存分配时机-栈扩容

go 复制代码
// src/runtime/stack.go::838
func copystack(gp *g, newsize uintptr) {
	// ...略

	// 分配新的栈空间
	new := stackalloc(uint32(newsize))

    // ...略
}

结论:创建Goroutine和栈扩容时,栈内存的分配都是由函数stackalloc分配。

所以,我们通过分析stackalloc函数就可以知道栈内存的分配过程了,具体如下。

栈内存分配过程

Go语言栈内存的分配按待分配的栈大小分为两大类:

  • 小于32KB的栈内存
  • 大于32KB的栈内存

小于32KB栈分配过程

  1. 先去M线程缓存mcache的栈内存缓存stackcache中分配:
  1. 如果stackcache内存不足,则从全局栈内存缓存池stackpool中分配:
  1. 如果stackpool内存不足,则从逻辑处理器结构p中的p.pagecache中分配:
  1. 如果p.pagecache内存不足,则从堆mheap中分配:

大于等于32KB栈分配过程

  1. 直接从全局栈内存缓存池stackLarge中分配:
  1. 全局栈内存缓存池stackLarge不足,则从逻辑处理器结构p中的p.pagecache中分配,如果p.pagecache则去堆上mheap分配:

Go语言堆内存的分配

Go语言堆内存分配时机

判断一个变量是否应该分配到「堆内存」的关键点就是:代码编译阶段,编译器会通过逃逸分析判断并标记上该变量是否需要分配到堆上。

通常我们在创建如下变量时,变量都有可能被分配到堆上:

切片分配过程源代码如下:

go 复制代码
// 代码位置:src/cmd/compile/internal/gc/walk.go::1316
// 初始化切片
case OMAKESLICE:
    // ...略...
    // 逃逸标识,是否需要逃逸到堆上
    if n.Esc == EscNone {
        // ...略...

        // 不需要逃逸
        // 直接栈上分配内存
        t = types.NewArray(t.Elem(), i) // [r]T
        
        // ...略...
    } else {
        // 需要内存逃逸到堆上
        
        // ...略...

        // 默认使用makeslice64函数从堆上分配内存
        fnname := "makeslice64"
        argtype := types.Types[TINT64]

        // ...略...

        if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
            (cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
            // 校验通过,则
            // 使用makeslice函数从堆上分配内存
            fnname = "makeslice"
            argtype = types.Types[TINT]
        }

        // ...略...

        // 调用上面指定的runtime函数
        m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))

        // ...略...
    }

最终分配堆内存的地方都会依赖函数mallocgc,我们通过阅读mallocgc的代码就可以看到堆内存的分配过程。

Go语言堆内存分配过程

堆内存的分配按对象的大小分,主要分为三大类:

  • 微对象 0 < Micro Object < 16B
  • 小对象 16B =< Small Object <= 32KB
  • 大对象 32KB < Large Object

「微对象」和「小对象」通常通过逻辑处理器结构P的线程缓存mcache分配,「大对象」直接从堆上mheap中分配,如下图所示:

线程缓存mcache的tiny结构主要负责分配「微对象」

线程缓存mcache的alloc结构主要负责分配「小对象」

微对象的分配过程

微对象 0 < Micro Object < 16B

  1. 线程缓存mcache的tiny内存充足,则直接分配「微对象」所需内存,图示如下:
  1. 线程缓存mcache的tiny内存不足,先去线程缓存mcache的alloc申请16B给tiny,再分配「微对象」所需内存,简易图示如下:

申请16B详细过程图示如下:

小对象的分配过程

小对象 16B =< Small Object <= 32KB

线程缓存mcache的alloc充足,则直接分配「小对象」所需内存,简易图示如下:

详细分配过程图示如下:

  1. 线程缓存mcache的alloc不足,则去中央缓存mcentral获取一个mspan,再分配「小对象」所需内存,图示如下:
  1. 线程缓存mcache的alloc不足,且中央缓存mcentral不足,则去逻辑处理器结构的p.pagecache分配,如果pagecache不足,直接去堆上mheap获取一个mspan,再分配「小对象」所需内存,图示如下:

大对象的分配过程

大对象 32KB < Large Object

  1. 逻辑处理器结构的pagecache充足,则直接分配「大对象」所需内存,图示如下:
  1. 逻辑处理器结构的pagecache不足,则直接去堆上mheap分配「大对象」所需内存,图示如下:

总结

Go内存管理

现代高级编程语言管理内存的方式分自动和手动两种。手动管理内存的典型代表是C和C++,编写代码过程中需要主动申请或者释放内存;而Java 和Go等语言使用自动的内存管理系统,由内存分配器和垃圾收集器来代为分配和回收内存,开发者只需关注业务代码而无需关注底层内存分配和回收,虽然语言帮我们处理了这部分但是还是有必要去了解一下底层的架构设计和执行逻辑,这样可以更好的掌握一门语言,本文主要以go内存管理为切入点再到go垃圾回收,系统的讲解一下go自动内存管理系统的设计和原理,由于篇幅有限略去了go垃圾回收三色标记屏障技术这一块,有兴趣的推荐去看下《go语言设计和实现》。

1. TCMalloc​

go内存管理是借鉴了TCMalloc的设计思想,TCMalloc全称Thead-Caching Malloc,是google开发的内存分配器,为了方便理解下面的go内存管理,有必要要先熟悉一下TCMalloc。

1.1 Page

操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。

1.2 Span

一组连续的Page被称为Span ,比如可以有4个页大小的Span,也可以有8个页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位

1.3 ThreadCache

每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。

1.4 CentralCache

是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同 ,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的

1.5 PageHeap

PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时候,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap

1.6 TCMalloc对象分配

小对象直接从ThreadCache分配,若ThreadCache不够则从CentralCache中获取内存,CentralCache内存不够时会再从PageHeap获取内存,大对象在PageHeap中选择合适的页组成span用于存储数据。

2. Go内存管理

经过上一节对TCMalloc内存管理的描述,对接下来理解go的内存管理会有大致架构的熟悉,go内存管理架构取之TCMalloc不过在细节上有些出入,先来看一张go内存管理的架构图

2.1 Page

和TCMalloc中page相同,上图中最下方浅蓝色长方形代表一个page

2.2 Span

与TCMalloc中的Span相同,Span是go内存管理的基本单位,代码中为 mspan,一组连续的Page组成1个Span,所以,上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span,另外,1个淡紫色长方形为1个Span

2.3 mcache

mcache与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span ,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问 。但mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache,因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问,下图是G,P,M三者之间的关系

2.4 mcentral

mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问,它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。但mcentral与CentralCache也有不同点,CentralCache是每个级别的Span有1个链表,mcache是每个级别的Span有2个链表。 【如果这里的级别指的是spanClass 那么就是一种级别有两种,scan类(包含指针的)和noscan类(不包含指针的)】

2.5 mheap

mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS(系统)申请出的内存页组成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的 。但mheap与PageHeap也有不同点:mheap把Span组织成了树结构,而不是链表,并且还是2棵树,然后把Span分配到heapArena进行管理,它包含地址映射和span是否包含指针等位图,这样做的主要原因是为了更高效的利用内存:分配、回收和再利用。【实际是建立基数树索引来快速查找空闲内存的】

2.6 内存分配

Go中的内存分类并不像TCMalloc那样分成小、中、大对象,但是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象小对象和大对象只用大小划定,无其他区分,其中小对象大小在16Byte到32KB之间,大对象大小大于32KB 。span规格分类【这就是简单的划分一个span可以有多少个bytes,同时一个object占多少bytes】 上面说到go的内存管理基本单位是span,且span有不同的规格,要想区分出不同的span,我们必须要有一个标识,每个span通过spanclass标识属于哪种规格的span,golang的span规格一共有67种,具体如下:

/usr/local/go/src/internal/runtime/gc/sizeclasses.go

go 复制代码
// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//     5         48        8192      170          32     31.52%         16
//     6         64        8192      128           0     23.44%         64
//     7         80        8192      102          32     19.07%         16
//     8         96        8192       85          32     15.95%         32
//     9        112        8192       73          16     13.56%         16
//    10        128        8192       64           0     11.72%        128
//    11        144        8192       56         128     11.82%         16
//    12        160        8192       51          32      9.73%         32
//    13        176        8192       46          96      9.59%         16
//    14        192        8192       42         128      9.25%         64
//    15        208        8192       39          80      8.12%         16
//    16        224        8192       36         128      8.15%         32
//    17        240        8192       34          32      6.62%         16
//    18        256        8192       32           0      5.86%        256
//    19        288        8192       28         128     12.16%         32
//    20        320        8192       25         192     11.80%         64
//    21        352        8192       23          96      9.88%         32
//    22        384        8192       21         128      9.51%        128
//    23        416        8192       19         288     10.71%         32
//    24        448        8192       18         128      8.37%         64
//    25        480        8192       17          32      6.82%         32
//    26        512        8192       16           0      6.05%        512
//    27        576        8192       14         128     12.33%         64
//    28        640        8192       12         512     15.48%        128
//    29        704        8192       11         448     13.93%         64
//    30        768        8192       10         512     13.94%        256
//    31        896        8192        9         128     15.52%        128
//    32       1024        8192        8           0     12.40%       1024
//    33       1152        8192        7         128     12.41%        128
//    34       1280        8192        6         512     15.55%        256
//    35       1408       16384       11         896     14.00%        128
//    36       1536        8192        5         512     14.00%        512
//    37       1792       16384        9         256     15.57%        256
//    38       2048        8192        4           0     12.45%       2048
//    39       2304       16384        7         256     12.46%        256
//    40       2688        8192        3         128     15.59%        128
//    41       3072       24576        8           0     12.47%       1024
//    42       3200       16384        5         384      6.22%        128
//    43       3456       24576        7         384      8.83%        128
//    44       4096        8192        2           0     15.60%       4096
//    45       4864       24576        5         256     16.65%        256
//    46       5376       16384        3         256     10.92%        256
//    47       6144       24576        4           0     12.48%       2048
//    48       6528       32768        5         128      6.23%        128
//    49       6784       40960        6         256      4.36%        128
//    50       6912       49152        7         768      3.37%        256
//    51       8192        8192        1           0     15.61%       8192
//    52       9472       57344        6         512     14.28%        256
//    53       9728       49152        5         512      3.64%        512
//    54      10240       40960        4           0      4.99%       2048
//    55      10880       32768        3         128      6.24%        128
//    56      12288       24576        2           0     11.45%       4096
//    57      13568       40960        3         256      9.99%        256
//    58      14336       57344        4           0      5.35%       2048
//    59      16384       16384        1           0     12.49%       8192
//    60      18432       73728        4           0     11.11%       2048
//    61      19072       57344        3         128      3.57%        128
//    62      20480       40960        2           0      6.87%       4096
//    63      21760       65536        3         256      6.25%        256
//    64      24576       24576        1           0     11.45%       8192
//    65      27264       81920        3         128     10.00%        128
//    66      28672       57344        2           0      4.91%       4096
//    67      32768       32768        1           0     12.50%       8192

// alignment  bits  min obj size
//         8     3             8
//        16     4            32
//        32     5           256
//        64     6           512
//       128     7           768
//      4096    12         28672
//      8192    13         32768

由上表可见最大的对象是32KB大小超过32KB大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。所以上面只有列出了1-66。 内存大小转换 下面还要三个数组,分别是:class_to_size ,size_to_class 和 class_to_allocnpages3个数组,对应下图上的3个箭头:

以第一列为例,类别1的对象大小是8bytes,所以 class_to_size[1]=8 ;span大小是8KB,为1页,所以 class_to_allocnpages[1]=1,下为go源码中大小转换数组。

/usr/local/go/src/internal/runtime/gc/sizeclasses.go

go 复制代码
// size class to obj size (byte)
var SizeClassToSize = [NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

// siez class to pages (page)
var SizeClassToNPages = [NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

// obj size to size class
var SizeClassToDivMagic = [NumSizeClasses]uint32{0, ^uint32(0)/8 + 1, ^uint32(0)/16 + 1, ^uint32(0)/24 + 1, ^uint32(0)/32 + 1, ^uint32(0)/48 + 1, ^uint32(0)/64 + 1, ^uint32(0)/80 + 1, ^uint32(0)/96 + 1, ^uint32(0)/112 + 1, ^uint32(0)/128 + 1, ^uint32(0)/144 + 1, ^uint32(0)/160 + 1, ^uint32(0)/176 + 1, ^uint32(0)/192 + 1, ^uint32(0)/208 + 1, ^uint32(0)/224 + 1, ^uint32(0)/240 + 1, ^uint32(0)/256 + 1, ^uint32(0)/288 + 1, ^uint32(0)/320 + 1, ^uint32(0)/352 + 1, ^uint32(0)/384 + 1, ^uint32(0)/416 + 1, ^uint32(0)/448 + 1, ^uint32(0)/480 + 1, ^uint32(0)/512 + 1, ^uint32(0)/576 + 1, ^uint32(0)/640 + 1, ^uint32(0)/704 + 1, ^uint32(0)/768 + 1, ^uint32(0)/896 + 1, ^uint32(0)/1024 + 1, ^uint32(0)/1152 + 1, ^uint32(0)/1280 + 1, ^uint32(0)/1408 + 1, ^uint32(0)/1536 + 1, ^uint32(0)/1792 + 1, ^uint32(0)/2048 + 1, ^uint32(0)/2304 + 1, ^uint32(0)/2688 + 1, ^uint32(0)/3072 + 1, ^uint32(0)/3200 + 1, ^uint32(0)/3456 + 1, ^uint32(0)/4096 + 1, ^uint32(0)/4864 + 1, ^uint32(0)/5376 + 1, ^uint32(0)/6144 + 1, ^uint32(0)/6528 + 1, ^uint32(0)/6784 + 1, ^uint32(0)/6912 + 1, ^uint32(0)/8192 + 1, ^uint32(0)/9472 + 1, ^uint32(0)/9728 + 1, ^uint32(0)/10240 + 1, ^uint32(0)/10880 + 1, ^uint32(0)/12288 + 1, ^uint32(0)/13568 + 1, ^uint32(0)/14336 + 1, ^uint32(0)/16384 + 1, ^uint32(0)/18432 + 1, ^uint32(0)/19072 + 1, ^uint32(0)/20480 + 1, ^uint32(0)/21760 + 1, ^uint32(0)/24576 + 1, ^uint32(0)/27264 + 1, ^uint32(0)/28672 + 1, ^uint32(0)/32768 + 1}
var SizeToSizeClass8 = [SmallSizeMax/SmallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
var SizeToSizeClass128 = [(MaxSmallSize-SmallSizeMax)/LargeSizeDiv + 1]uint8{32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}

为对象寻找span 寻找span的流程如下:

  1. 计算对象所需内存大小size

  2. 根据size到size class映射,计算出所需的size class

  3. 根据size class和对象是否包含指针计算出span class

  4. 获取该span class指向的span

以分配一个包含指针大小为20Byte的对象为例,根据映射表:

go 复制代码
// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8

size class 3,它的对象大小范围是(16,32]Byte,20Byte刚好在此区间,所以此对象的size class为3,Size class到span class的计算如下:

/usr/local/go/src/runtime/mheap.go

go 复制代码
// noscan为false代表对象包含指针
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

所以,对应的span class为:

go 复制代码
span class = 3 << 1 | 0 = 6

所以该对象需要的是span class 6指向的span,自此,小对象内存分配完成。

/usr/local/go/src/runtime/malloc.go

go 复制代码
var sizeclass uint8
	// step1: 确定规格sizeclass
	if size <= gc.SmallSizeMax-8 {
		sizeclass = gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
	} else {
		sizeclass = gc.SizeToSizeClass128[divRoundUp(size-gc.SmallSizeMax, gc.LargeSizeDiv)]
	}
	size = uintptr(gc.SizeClassToSize[sizeclass])
	// size class到span class
	spc := makeSpanClass(sizeclass, false)
	//step2: 分配对应spanclass 的 span
	span := c.alloc[spc]
	v := nextFreeFast(span)
	if v == 0 {
		v, span, checkGCTrigger = c.nextFree(spc)
	}
	x := unsafe.Pointer(v)
	if span.needzero != 0 {
		memclrNoHeapPointers(x, size)
	}

大对象(>32KB)的分配则简单多了,直接在 mheap 上进行分配,首先计算出需要的内存页数和span class级别,然后优先从 free 中搜索可用的span,如果没有找到,会从 scav 中搜索可用的span,如果还没有找到,则向OS申请内存,再重新搜索2棵树,必然能找到span。如果找到的span比需求的span大,则把span进行分割成2个span,其中1个刚好是需求大小,把剩下的span再加入到 free 中去。

3 垃圾回收

3.1 标记-清除

标记-清除算法是第一种自动内存管理,基于追踪的垃圾收集算法。算法思想在 70 年代就提出了,是一种非常古老的算法。内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。算法分为两个部分:标记(mark)和清除(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。可视化可以参考下图。

标记-清除算法的优点就是基于追踪的垃圾回收算法具有的优点:避免了引用计数算法的缺点(不能处理循环引用,需要维护指针)【标记-清除通过"从根出发的可达性分析"处理循环引用:不可达的整圈都会在 sweep 时一起被回收】。缺点也很明显,需要STW。

3.2 三色可达性分析

三色标记算法是对标记阶段的改进,原理如下:

  1. 起初所有对象都是白色。

  2. 从根出发扫描所有可达对象,标记为灰色,放入待处理队列。

  3. 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。

  4. 重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收

三色标记的一个明显好处是能够让用户程序和 mark 并发的进行 ,不过三色标记清除算法本身是不可以并发或者增量执行的,它需要STW,而如果并发执行,用户程序可能在标记执行的过程中修改对象的指针,导致可能将本该死亡的对象标记为存活和本该存活的对象标记为死亡,为了解决这种问题,go v1.8之后使用混合写屏障技术支持并发和增量执行,将垃圾收集的时间缩短至0.5ms以内。

3.3 gc触发

在堆上分配大于 32K byte 对象的时候进行检测此时是否满足垃圾回收条件,如果满足则进行垃圾回收

go 复制代码
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    ...
    shouldhelpgc := false
    // 分配对象 > 32K byte
    if size <= maxSmallSize {
        ...
    } else {
        shouldhelpgc = true
        ...
    }
    ...
    // gcShouldStart() 函数进行触发条件检测
    if shouldhelpgc && gcShouldStart(false) {
        // gcStart() 函数进行垃圾回收
        gcStart(gcBackgroundMode, false)
    }
}
// GC runs a garbage collection and blocks the caller until the​ 
// garbage collection is complete. It may also block the entire​ 
// program.
func GC() {
    gcStart(gcForceBlockMode, false)
}

上面是自动垃圾回收,还有一种主动垃圾回收,通过调用 runtime.GC(),这是阻塞式的。

go 复制代码
// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
    gcStart(gcForceBlockMode, false)
}

系统gc触发条件:触发条件主要关注下面代码中的中间部分: forceTrigger || memstats.heap_live >= memstats.gc_trigger,forceTrigger 是 forceGC 的标志,后面半句的意思是当前堆上的活跃对象大于我们初始化的时候设置的 GC 触发阈值,在 malloc 以及 free 的时候 heap_live 会一直进行更新

go 复制代码
// gcShouldStart returns true if the exit condition for the GCoff
// phase has been met. The exit condition should be tested when
// allocating.
//
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
    return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}

//初始化的时候设置 GC 的触发阈值
func gcinit() {
    _ = setGCPercent(readgogc())
    memstats.gc_trigger = heapminimum
    ...
}

// 启动的时候通过 GOGC 传递百分比 x
// 触发阈值等于 x * defaultHeapMinimum (defaultHeapMinimum 默认是 4M)
func readgogc() int32 {
    p := gogetenv("GOGC")
    if p == "off" {
        return -1
    }
    if n, ok := atoi32(p); ok {
        return n
    }
    return 100
}

3.4 gc过程

下列源码是基于 go 1.8,由于源码过长,所以这里只尽量只关注主流程

  • gcStart
go 复制代码
// gcStart 是 GC 的入口函数,根据 gcMode 做处理。
// 1. gcMode == gcBackgroundMode (后台运行,也就是并行), _GCoff -> _GCmark
// 2. 否则 GCoff -> _GCmarktermination, 这个时候是主动 GC
func gcStart(mode gcMode, forceTrigger bool) {
    ...
    //在后台启动 mark worker
    if mode == gcBackgroundMode {
        gcBgMarkStartWorkers()
    }
    ...
    // Stop The World
    systemstack(stopTheWorldWithSema)
    ...
    if mode == gcBackgroundMode {
        // GC 开始前的准备工作

        //处理设置 GCPhase, setGCPhase 还会 开始写屏障
        setGCPhase(_GCmark)

        gcBgMarkPrepare() // Must happen before assist enable.
        gcMarkRootPrepare()

		// Mark all active tinyalloc blocks. Since we're
		// allocating from these, they need to be black like
		// other allocations. The alternative is to blacken
		// the tiny block on every allocation from it, which
		// would slow down the tiny allocator.
		gcMarkTinyAllocs()
		
		// Start The World
		systemstack(startTheWorldWithSema)
	} else {
	    ...
	}
}
  • Mark
go 复制代码
func gcStart(mode gcMode, forceTrigger bool) {
	...
	//在后台启动 mark worker
	if mode == gcBackgroundMode {
		gcBgMarkStartWorkers()
	}
}

func gcBgMarkStartWorkers() {
	// Background marking is performed by per-P G's. Ensure that
	// each P has a background GC G.
	for _, p := range &allp {
		if p == nil || p.status == _Pdead {
			break
		}
		if p.gcBgMarkWorker == 0 {
			go gcBgMarkWorker(p)
			notetsleepg(&work.bgMarkReady, -1)
			noteclear(&work.bgMarkReady)
		}
	}
}

// gcBgMarkWorker 是一直在后台运行的,大部分时候是休眠状态,通过 gcController 来调度

func gcBgMarkWorker(_p_ *p) {
	for {
		// 将当前 goroutine 休眠,直到满足某些条件
		gopark(...)
		...

		// mark 过程
		systemstack(func() {
			// Mark our goroutine preemptible so its stack
			// can be scanned. This lets two mark workers
			// scan each other (otherwise, they would
			// deadlock). We must not modify anything on
			// the G stack. However, stack shrinking is
			// disabled for mark workers, so it is safe to
			// read from the G stack.
			casgstatus(gp, _Grunning, _Gwaiting)
			switch _p_.gcMarkWorkerMode {
			default:
				throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
			case gcMarkWorkerDedicatedMode:
				gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
			case gcMarkWorkerFractionalMode:
				gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
			case gcMarkWorkerIdleMode:
				gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
			}
			casgstatus(gp, _Gwaiting, _Grunning)
		})
	}
}

Mark 阶段的标记代码主要在函数 gcDrain() 中实现

go 复制代码
// gcDrain scans roots and objects in work buffers, blackening grey
// objects until all roots and work buffers have been drained.
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	...

	// Drain root marking jobs.
	if work.markrootNext < work.markrootJobs {
		for !(preemptible && gp.preempt) {
			job := atomic.Xadd(&work.markrootNext, +1) - 1
			if job >= work.markrootJobs {
				break
			}
			markroot(gcw, job)
			if idle && pollWork() {
				goto done
			}
		}
	}

	// 处理 heap 标记
	// Drain heap marking jobs.
	for !(preemptible && gp.preempt) {
		...
		//从灰色队列中取出对象
		var b uintptr
		if blocking {
			b = gcw.get()
		} else {
			b = gcw.tryGetFast()
			if b == 0 {
				b = gcw.tryGet()
			}
		}
		if b == 0 {
			// work barrier reached or tryGet failed.
			break
		}
		//扫描灰色对象的引用对象,标记为灰色,入灰色队列
		scanobject(b, gcw)
	}
}
  • Sweep
go 复制代码
func gcSweep(mode gcMode) {
	...
	//阻塞式
	if !_ConcurrentSweep || mode == gcForceBlockMode {
		// special case synchronous sweep.
		...
		// Sweep all spans eagerly.
		for sweepone() != ^uintptr(0) {
			sweep.npausesweep++
		}

		// Do an additional mProf_GC, because all 'free' events are now real as well.
		mProf_GC()
		mProf_GC()
		return
	}

	// 并行式
	// Background sweep.
	lock(&sweep.lock)
	if sweep.parked {
		sweep.parked = false
		ready(sweep.g, 0, true)
	}
	unlock(&sweep.lock)
}

对于并行式清扫,在 GC 初始化的时候就会启动 bgsweep(),然后在后台一直循环

go 复制代码
func bgsweep(c chan int) {
	sweep.g = getg()

	lock(&sweep.lock)
	sweep.parked = true
	c <- 1
	goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)

	for {
		for gosweepone() != ^uintptr(0) {
			sweep.nbgsweep++
			Gosched()
		}
		lock(&sweep.lock)
		if !gosweepdone() {
			// This can happen if a GC runs between
			// gosweepone returning ^0 above
			// and the lock being acquired.
			unlock(&sweep.lock)
			continue
		}
		sweep.parked = true
		goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
	}
}

func gosweepone() uintptr {
	var ret uintptr
	systemstack(func() {
		ret = sweepone()
	})
	return ret
}

不管是阻塞式还是并行式,最终都会调用 sweepone()。上面说过 go 内存管理都是基于 span 的,mheap_ 是一个全局的变量,所有分配的对象都会记录在 mheap_ 中。在标记的时候,我们只要找到对象对应的 span 进行标记,清扫的时候扫描 span,没有标记的 span 就可以回收了。

go 复制代码
// sweeps one span
// returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep
func sweepone() uintptr {
	...
	for {
		s := mheap_.sweepSpans[1-sg/2%2].pop()
		...
		if !s.sweep(false) {
			// Span is still in-use, so this returned no
			// pages to the heap and the span needs to
			// move to the swept in-use list.
			npages = 0
		}
	}
}

// Sweep frees or collects finalizers for blocks not marked in the mark phase.
// It clears the mark bits in preparation for the next GC round.
// Returns true if the span was returned to heap.
// If preserve=true, don't return it to heap nor relink in MCentral lists;
// caller takes care of it.
func (s *mspan) sweep(preserve bool) bool {
	...
}

参考文章

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
高山上有一只小老虎2 小时前
mybatisplus实现简单的增删改查方法
java·spring boot·后端
黎子越2 小时前
python循环相关联系
开发语言·python·算法
myloveasuka2 小时前
汉明编码的最小距离、汉明距离
服务器·数据库·笔记·算法·计算机组成原理
沛沛rh452 小时前
Rust浮点数完全指南:从基础到实战避坑
深度学习·算法·计算机视觉·rust
阿蒙Amon2 小时前
C#每日面试题-break、continue和goto的区别
java·面试·c#
近津薪荼2 小时前
优选算法——双指针1(数组分块)
c++·学习·算法
Дерек的学习记录2 小时前
二叉树(下)
c语言·开发语言·数据结构·学习·算法·链表
宏集科技工业物联网2 小时前
工业自动化的演进路径:X平台如何随行业发展不断进化
经验分享·自动化·工业物联网·工业自动化·工业自动化平台
计算机程序设计小李同学2 小时前
基于位置服务的二手图书回收平台
java·前端·vue.js·spring boot·后端