万字长文图解 Go 内存管理分析:工具、分配和回收原理

栏目: IT技术 · 发布时间: 3年前

 golang的内存分析 工具 怎么用?内存和回收原理,这一篇就够了

大纲

  • 1. 目录

  • 2. 由一个问题展开

  • 3. 名字说明

  • 4. 内存怎么采样?

    • 4.1 编译期间逃逸分析

    • 4.2 采样的简单实现

    • 4.3 内存采样的时机

    • 4.4 内存采样的入口

    • 4.5 内存采样的信息

    • 4.6 golang的类型反射

  • 5. 内存分配

    • 5.1 C语言 你分配和释放内存怎么做?

    • 5.2 内存分配设计考虑的几个问题

    • 5.3 golang的内存分配

  • 6. 内存回收

    • 6.1 golang协程抢占执行

    • 6.2 STW是怎么回事?

    • 6.3 垃圾回收要求

    • 6.4 golang版本迭代历史

    • 6.5 GC触发条件

    • 6.6 三色定义

    • 6.7 GC流程

    • 6.8 写屏障

    • 6.9 内存可见性

    • 6.10 注意问题

1. 目录

2. 由一个问题展开

golang从语言级别,就提供了完整的采样和分析的机制。大家经常使用 pprof 分析内存占用。

万字长文图解  <a href='https://www.codercto.com/topics/6127.html'>Go</a>  内存管理分析:工具、分配和回收原理

但是不清楚怎么实现?不清楚怎么看指标?不清楚 flat,cum的区别?我们就从这个问题展开。

3. 名字说明

内存分析的时候,有四个输入选项:

  1. alloc_objects : 历史总分配的累计

  2. alloc_space :历史总分配累计

  3. inuse_objects:当前正在使用的对象数

    1. 堆上分配出来,业务正在使用的,也包括业务没有使用但是还没有垃圾回收掉的对象。

  4. inuse_space:当前正在使用的内存

两个输出选项:

  1. flat:平坦分配,非累加

  2. cum:累加

思考几个问题:

  1. 上面说的对象是什么概念?

  2. 经常使用内存分析,这个内存分析是否是精确的?性能消耗大不大

  3. 为啥显示的是堆栈?不是说分配的对象吗?为啥不直接显示分配的对象结构名?

4. 内存怎么采样?

4.1 编译期间逃逸分析

说明下,golang pprof是分析从堆上分配的内存。golang的内存在堆上,还是在栈上?这个不是我们决定的,就算你调用new这个关键字,也不一定是在堆上分配。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

万字长文图解 Go 内存管理分析:工具、分配和回收原理

逃逸分析是golang的一个非常重要的一个点。 对于内存分配,垃圾回收的设计都有非常重要的影响。

4.2 采样的简单实现

采样的实现非常简单。简单描述流程:

  1. 用一个公共变量用来记录

  2. 分配内存的时候,加alloc size,加alloc对象数

  3. 释放内存的时候,加free size,加free对象数

累计分配:就是alloc 当前在用 inuse:就是 alloc-free

万字长文图解 Go 内存管理分析:工具、分配和回收原理

4.3 内存采样的时机

采样的时机说3个点:

  1. 分配堆上内存的时候,累计分配

  2. 回收器释放堆上内存的时候,累计释放

  3. 每512KB打点采样

但是注意一点:并不是每一次分配内存都会被采样。也就是说这里其实是有个权衡的。现在是每满512KB才会采样一次。这里的考虑是性能和采样效果的权衡。因为采样是要耗费性能的,是要取堆栈的。

怎么理解?举个例子

理想情况下(不考虑其他任何影响):

万字长文图解 Go 内存管理分析:工具、分配和回收原理

那么有人会想,这样岂不是会漏掉了很多内存?统计还能用来排查问题吗?

这个是性能和效果的一个考虑,一般来讲,我们是用pprof分析内存占用的时候,在整个golang程序跑起来后,时时刻刻都在分配释放内存,每累计分配512KB,打点一次。虽然会漏掉一些内存分配释放,但是对每个结构都是公平的。如果有一个内存泄露分配行为,那么累计下来一定会被抓住的,并且是非常容易被抓住。

4.4 内存采样的入口

内存采样的入口,这个非常简单理解。肯定是一个在分配内存的函数位置,一个是释放内存的位置。这里要特意提下上下文环境。因为golang是垃圾回收类型的语言,内存分配是完全交由golang自己管理,自己不能管理内存。

两个入口函数:

  1. mProf_Malloc

  2. mProf_Free

这两个是配套使用的采样打点函数。而且一定是配套的。简单说:

  • mProf_Malloc 是由业务程序行为(赋值器)触发的,分配内存嘛。比如你new了一个对象,这个对象在堆上,那么会调用 mallocgc 分配内存,如果到了采样点,那么会调用 mProf_Malloc 采样。
  • mProf_Free 是回收器在确定并且将要回收内存的时候调用的。是垃圾回收过程的一环。并且还要注意一点,只有打过点的(mProf_Malloc计数过的对象,会有一个特殊处理),才会配套使用mProf_Free。

    • 不是说,任意给一个内存地址给你。你都知道这个是业务类型。

4.5 内存采样的信息

这里问你的是,golang采样是采样啥?类型信息?这里也说过一点,内存这里和类型系统是没啥关系的。这里采样的是分配栈,也就是分配路径。

4.5.1 flat,cum 分别是怎么来的?

看个例子: 万字长文图解 Go 内存管理分析:工具、分配和回收原理

大家可以先猜下,我们看alloc_space。这个内存会是怎么累计到的。实际统计如下:

万字长文图解 Go 内存管理分析:工具、分配和回收原理

和大家猜的一样吗?这些是怎么看。

首先说几个结论:

  1. flat统计到的,就是这个函数实际分配的。

  2. cum是累计的,包含自己分配的,也包含路过的。

  • cum和flat不相同的时候,代表这个函数除了自己分配内存,自己内部调用的别的函数也在分配内存。

重点提示:这个要理解这个,首先要知道,内存采样的是什么,内存采样的是分配栈。

解释说明

(图中140M我们当150M看哈,这里采样少了第一次,细节原因可以看代码,这里提一下,不做阐述。):

  1. main函数里,A函数调用了5次,B函数 5次,C函数5次。其中B会调用A,C会调用B。

  2. 调用一次A会分配10M内存,调用一次B会分配20M,调用一次C会分配30M。总累计分配内存是300M

  3. A函数实际调用次数是 15次;这个和flat的值是一致的:150M

    1. (A) * 5

    2. (B -> A) * 5

    3. (C -> B -> A) * 5

  4. B函数函数实际调用10次;这个和flat的值也是一致的:100M

    1. B * 5

    2. (C -> B) * 5

  5. C函数5次:这个和flat的值是一致的:50M

    1. C * 5

  6. main函数300M,也是一致的。

图示

万字长文图解 Go 内存管理分析:工具、分配和回收原理

记住一句话:采样是记录分配堆栈,而不是类型信息。

4.6 golang的类型反射

思考几个问题:

  1. 任意给一个内存地址给你,能知道这个对象类型吗?

  2. golang的反射到底是怎么回事?

先说结论:golang里面,内存块是没有携带对象类型信息的,这个跟C是一样的。但是golang又有反射,golang的反射一定要基于interface使用。这个要仔细理解下。

因为,golang里面interface的结构变量,是会记录type类型的。

反射定律一:反射一定是基于接口的。是从接口到反射类型。

反射定律二:反射一定是基于接口的。是从反射类型到接口。

还是那句话,golang的反射一定是依赖接口类型的,一定是经过接口倒腾过的。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

因为当前接口这个类型对应了两个内部结构: struct ifacestruct eface ,这两个结构都是会存储type类型。以后的一切都是基于这个类型的。

5. 内存分配

5.1 C语言你分配和释放内存怎么做?

思考一个问题,在C语言里,我们分配内存:

分配内存的时候,传入大小,拿到一个指针。

ptr = malloc(1024);

释放内存的时候,直接传入ptr,没有任何其他参数:

free (ptr);

释放的时候,怎么确定释放哪些位置?如果要你自己实现,有很多简单的思路,说一个最简单的:分配的时候,不止分配1024字节,还分配了其他的信息,带head了。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

这种分配方式有什么问题:

  1. 开销大,在通用的内存分配器中,很多场景下,有可能meta信息比自身还要大。

5.2 内存分配设计考虑的几个问题

  1. 性能

    1. 局部性

  2. 碎片率

    1. 内部碎片率

    2. 外部碎片率

5.3 golang的内存分配

golang大方向的考虑就是基于局部性和碎片率来考虑的。使用的是和tcmalloc一致的设计。

5.3.1 整体设计

首先,内存块是不带类型信息的。像我们在C语言里面,有时候实现的简单的内存池,在不考虑一些开销的时候,会把业务类型放到meta信息里,为的是排查问题方便。golang内存管理作为一个通用模块,不会这么搞。

5.3.1.1 地址空间设计

很多时候,你查golang的资料,会看到这张图:

万字长文图解 Go 内存管理分析:工具、分配和回收原理

这张图有几个信息比较重要:

  1. 为什么spans区域是512M,bitmap区是16G,arena是512G?先不要纠结值,我们先说这个比例关系:

    1. spans区域,一个指针大小(8Byte)对应arena的一个page(8KB),倍数是1024

    2. bitmap区域,一个字节(8bit)对应arena的32Bytes,倍数是32倍

  2. 我们给用户分配的内存就是arena区域的内存,spans区,bitmap区均为其他用途的元数据信息。

    1. bitmap这个实现我们这次不谈,不同通过这个你得知道一点:并不是所有的内存空间都会扫描一把,是有挑选判断的。

    2. spans区域是一般用来根据一个内存地址查询mspan结构的。调用函数:spanOf。

    3. bitmap是用来辅助垃圾回收用的区域。有这个bitmap信息可以提高回收效率和精度。注意一点,这个不是标识object是否分配的位图,标识是否分配object的问题是 mspan.allocBits 结构。这个可以理解为提高垃圾回收效率的实现。

注意几个点:

  1. 很多文章都提到golang内存512GB这个事情。512GB说的是内存虚拟地址空间的限制,是最大能力,是最大的规划利用。golang之前最大可以使用的内存地址空间。

  2. golang1.11 之后已经没有512GB的限制了。基本上和系统的虚拟地址空间一致

    1. 这个比例还是一样的,1:1024,1:32

  3. 就算golang1.11之前,也不是说golang的程序上来就向系统申请这么大块虚拟地址。也是每64M的申请,管理对象单元是heapArea结构。

  4. 三个区域看着连续结在一起,但是其实不是连续的地址。

    1. 实际的实现中都是以64M(heapArena)的小单位进行的。

5.3.2 抽象对象概念

万字长文图解 Go 内存管理分析:工具、分配和回收原理

物理偏向概念:

  1. heapArena:堆上物理空间管理的一个小单元,64M一个。

  2. page:物理内存最小单位,8KB一个。

逻辑偏向概念:

  1. span:span为内存分配的一个管理单元。span内按照固定大小size划分,相同的size划分为同一类。一个span管理一个连续的page。

  2. object:内存分配的最小单元。

管理结构层次概念:

mcache:每个M上的,管理内存用的。我们都知道GMP架构,每个M都有自己的内存cache管理,这样是为了局部性。只是一个cache管理。mcentral:mheap结构所有,也只是一个cache管理,但是是为所有人服务的。mheap:是真正负责分配和释放物理内存的。

5.3.3 局部性的设计

这个思路很简单,就是设计成局部性的一个层次设计。

5.3.3.1 mcache

mcache由于只归属自己的M,span一旦在这个结构管理下,其他人是不可见,不会去操作的。只有这个m会操作。所以自然就不需要加锁。

5.3.3.2 mcentral

mcentral是所有人可见的。所以操作自然要互斥,这个的作用也是一个cache的统一管理。

5.3.3.3 mheap

这个是负责真实内存分配和释放的的一个结构。

5.3.4 针对碎片率的设计

golang的内存设计目标:碎片率平均12.5%左右。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

说明:

  1. tail wast实际是浪费的外部碎片

    1. 比如说,第一种size,8字节。一个page 8KB,8字节刚好对齐。外部碎片为0.

  2. max waste说的是最大的内部碎片率

    1. 怎么算的?每一个放进该span的对象大小都是最小值的情况

    2. 比如说,第一种size,8字节。最小的对象是1字节,浪费7字节,最大碎片率为 1-1/8 = 87.5%

怎么的出来的这些值?经验值吧,可能。

6. 内存回收

6.1 golang协程抢占执行

首先,golang没有真正的抢占。golang调度单位为协程,所谓抢占,也就是强行剥夺执行权。但是有一点,golang本质上是非抢占的,不像操作系统那样,有时钟中断和时间片的概念。golang虽然里面是有一个抢占的概念,但是注意了,这个抢占是建议性质的抢占,也就是说,如果有协程不听话,那是没有办法的,实现抢占的效果是要对方协程自己配合的。

一句话:系统想让某个goroutine自己放弃执行权,会给这个协程设置一个魔数,协程在切调度,或者其他时机检查到了的时候,会感知到这一个行为。

当前的抢占实现是:

  1. 给这个协程设置一个的魔数(stackguard)。每个函数的入口会比较当前栈寄存器值和stackguard值来决定是否触发morestack函数。(这是一个抢占调度点)

  2. 协程调用函数的时候,会检查是否需要栈扩容。如果被设置了抢占标示,那么就会首先调用到

  3. 调用newstack,在newstack里面判断是否是特殊值,这种特殊值,目的不在于扩容,而在于让出调度。

所以,在golang里面,只要有函数调用,就会有感知抢占的时机。stw就是基于这个实现的。

思考一个问题:

如果有一个猥琐的函数:非常耗时,一直在做cpu操作,并且完全没有函数调用。这种情况下,golang是没有一点办法的。那么这种情况会影响到整个程序的能力。

所以,我们平时写函数,一定要短小精悍,功能拆分合理。

6.2 STW是怎么回事?

STW:stop the world,也就是说暂停说由协程的调度和执行。stw是怎么实现?stw的基础就是上面提到的抢占实现。stw调用的目的是为了让整个程序(赋值器停止),那么就需要剥夺每一个协程的执行。

stw在垃圾回收的几个关键操作里是需要的,比如开启垃圾回收,需要stw,做好准备工作。如果stw的时候,出现了猥琐的函数,那么会导致整个系统的能力降低。因为大家都在等你一个人。

6.3 垃圾回收要求

  1. 正确性:绝对不能回收正在使用的的内存对象。

  2. 存活性:一轮回收过程一定是有边界,可结束的。

6.4 golang版本迭代历史

  1. go 1.3 以前,使用是标记-清扫的方式,整个过程需要stw

  2. go 1.3 版本分离了标记和清扫操作,标记过程stw,清扫过程并发执行

  3. go 1.5 版本在标记过程中,使用三色标记法。回收过程分为四个阶段,其中,标记和清扫都并发执行的,但标记阶段的前后需要stw一定时间来做gc的准备工作和栈的re-scan。

  4. go 1.8 版本引入了混合写屏障机制,避免了对栈的re-scan,极大的减少了stw的时间。

6.5 GC触发条件

  • gcTriggerHeap 当分配的内存达到一定值就触发GC

  • gcTriggerTime 当一定时间没有执行过GC就触发

  • gcTriggerCycle 要求启动新一轮的GC,一启动则跳过,手动触发GC的runtime.GC( )会使用这个条件

6.6 三色定义

6.6.1 强三色

黑色对象不允许指向白色对象。

6.6.2 弱三色

黑色对象可以指向白色对象,但是前提是,该白色对象一定是处于灰色保护链中。

6.7 GC流程

这里不详细阐述了。贴一张go1.8之前的图:

万字长文图解 Go 内存管理分析:工具、分配和回收原理

当下GC大概分为四个阶段:

  1. GC准备阶段

  2. 标记阶段

  3. 标记结束阶段

  4. 清理阶段

6.8 写屏障

如果标记和回收不用和应用程序并发,在标记和回收整个过程直接stw,那么就简单了。golang为了提供低时延,就必须让赋值器和回收器并发起来。但是在并发的过程中,赋值器和回收器对于引用树的理解就会出现不一致,这里就一定要配合写屏障技术。

写屏障技术,是动态捕捉写操作,维持回收正确性的技术。写屏障就是一段 hook 代码,编译期间生成,运行期间跟进情况会调用到 hook 的代码段,也就是写屏障的代码;

下面系统整体的讨论下写屏障的技术。

6.8.1 插入写屏障

(Dijkstra '78)


   

writePointer ( slot, ptr ):

// 无脑保护插入的新值

shade ( ptr )

*slot = ptr

这个是另外一个通用的屏障技术。这个维护的是强三色不变式来保证正确性,保证黑色对象一定不能指向白色对象。golang使用的是这个屏障,插入屏障。按照道理,是几乎完全不需要stw的。但是golang有一个处理,由于栈上面使用屏障会导致处理非常复杂,并且开销会非常大。所以当前golang只针对堆上的写操作做了屏障。

那么就会带来一个问题:所以当一轮扫描完了之后,在标记结束的阶段,还需要重新扫描一遍goroutine栈,并且栈引用到的所有对象也要扫描。因为goroutine有可能直接指向了白色对象。在扫描goroutine栈过程中,需要stw。这个也是go1.8以前的一个非常大的延迟来源。

(开始的时候,stw扫描栈,得到灰色对象)

图表演示

堆上路径赋值:

step1:堆上对象赋值的时候,插入写屏障,保护强三色不变式

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step2:删除的时候,没啥问题

万字长文图解 Go 内存管理分析:工具、分配和回收原理

栈上对象赋值:

step3:栈上对象赋值的时候,没有写屏障。白色对象直接被黑色对象引用。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step4:删除灰色保护路径。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

所以才需要在mark terminato阶段,重新扫描栈。

6.8.2 删除写屏障

(Yuasa '90)


   

writePointer ( slot, ptr ):

// 删除之前,保护原先白色或者灰色指向的数据块

if ( isGery ( slot ) || isWhite ( slot ) )

shade ( *slot )

*slot = ptr

这个是通用的一种写屏障技术。golang并没有实现,而是实现了插入写屏障。原因就在于:这个在垃圾回收之前,必须做一个快照扫描,这个就会对用户时延有比较严重的影响。下面详述。

主要流程:

  1. 在标记之前,需要打一个引用关系的快照。所以,这个对于栈内存很大的时候,影响越大。

    1. 不需要完整的快照,只需要在扫描堆对象之前,确保所有的栈对象是黑色的。引用都是灰色的,这样就保证了一个前提:所有可达的对象都处于灰色保护状态中。

    2. 对栈快照扫描需要stw,去扫描栈对象。这个时候,是需要暂停所有的用户程序。

  2. 扫描堆对象的时候,可以和应用程序并发的。此后根一直保持黑色(黑色赋值器),不用再扫描栈。

  3. 对象被删除的时候,删除写屏障会捕捉到。置灰。

    1. 上面的伪代码显示有条件,其实第一版的时候是没有条件的。

    2. 这里加上条件是为了回收精度:当上游之前是白色或者灰色才需要把这个置灰色。如果是黑?那么一定是处于灰色保护状态,因为这个是前提(理解这个非常重要)。

(开始的时候,stw扫描栈,得到灰色对象)

图表演示

初始扫描快照后:

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step1: 赋值。这里赋值是允许的,虽然是破坏了强三色不变式。但是还是符合弱三色不变式。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step2:删除。这里就拦截了,必须置灰色。保证弱三色不变式。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

回收精度:

删除写屏障的精度比插入写屏障的精度更低。删除的即使是最后一个指针,也会保留到下一轮,属于一个浮动垃圾。这个比插入屏障精度还低。因为,对于插入屏障所保留的对象,回收器至少可以确定曾在其中执行了某些回收相关的操作(获取或写入对象的引用),但删除屏障所保留的对象却不一定被赋值器操作过。

为什么需要打快照?

删除写屏障,又叫快照屏障增量技术(或者说,一定要配合这个来做)。

  1. 首先,是需要stw,针对扫描整个栈根打做一遍扫描。相当于一个快照。这个过程扫描之后,就能保证当前(时刻)所有可达的对象都处于灰色保护状态,满足弱三色不变式。

  2. 然后,赋值器和回收器就可以并发。但是并发有可能会破坏导致弱三色不变式。这个时候,就需要删除写屏障来时刻保护白色对象。

golang为啥没有用这个?

  1. 一个是精度问题,这个精度要比插入写屏障低;

  2. 考虑goroutine可能非常多,不适合上来就stw,扫描所有的内存栈。这个适合小内存的场景。

    1. 思考一个问题:这个和混合写屏障有没有区别?还是有区别的,这里是要锁整个栈,混合写屏障是并发的,每次只需要锁单个栈。

6.8.3 混合写屏障

混合屏障是结合插入屏障和删除屏障。

伪代码:


   

writePointer (slot, ptr) :

// 保护原来的(被删除的)

shade ( *slot )

if current stack is grey:

// 如果对象为灰色,则还需要保护新指向的对象

shade ( ptr )

*slot = ptr

(开始的时候,stw扫描栈,得到黑色对象)

golang实际情况:

伪代码如上。但是这里提出来一点,golang根本不是和伪代码说的这样。没有做条件判断,所以现在的回收精度很低。这个算是一个TodoList。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

注意:使用了混合屏障,还是针对堆上的,栈上对象写入还是没有barrier。golang之前只使用插入屏障,关键在于栈对象没有,导致栈上黑对象可能指向白对象。所以要rescan。因为如果不rescan,而且又破坏了弱三色不变式(没有处于灰色保护链中),那么就丢数据了。

混合屏障,就是结合删除屏障,保护这一个前提,代价就是进一步降低回收精度。

图表示例:

混合屏障就是要解决: 栈指向白色对象,stw重新扫描栈的问题。

step1:赋值白对象到黑对象引用,这个不会阻止这个,也不会有写屏障。就是一个正常的赋值。

  1. 这个时候黑色指向了白色对象。破坏了强三色不变式。

  2. 但是这个白色对象还处于灰色状态保护下。符合弱三色不变式。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step2:删除指针的时候,意图破坏弱三色不变式的时候,写屏障就会把这个对象置灰色。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

问题一:如果有个还会想?由于栈上没有写屏障,这个删除的对象式根指向的呢?如果存在以下场景?

step1:堆上的白色对象引用赋值给黑色栈对象。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

step2:如果删除指针,岂不是连弱三色不变式也破坏了?

万字长文图解 Go 内存管理分析:工具、分配和回收原理

这个怎么办呢?

答案是:其实根本就不可能出现这个场景的引用图。第一个图就不会出现。因为虽然没有stw,但是扫描某个g的时候,这个g是暂停的。相当于这个g栈是一个快照状态。

混合写屏障的栈,要么全黑,要么全白(单个栈)

那么这个暂停g这个是怎么做到的?

  1. 扫描的时候,会设置一个 _Gscan 状态。

  2. casgstatus的时候,保证循环等待这个状态完成。之前是直接吃cpu的,后面做了一个优化,加了一个yield,5us的间隔。

    1. 关于这段代码的改动

    2. 万字长文图解 Go 内存管理分析:工具、分配和回收原理

问题二:如果是多个栈呢,那么就不是原子的快照了。比如下图?那么就可能导致这种情况。

万字长文图解 Go 内存管理分析:工具、分配和回收原理

如果说A和前面的黑色对象不属于同一个g栈。那么是否可能会导致这种场景出现?分析下:

  1. 这个场景是有这么一个白色对象,先只被G2栈根引用到。

  2. 当前G1已经被扫描完,G2还没有扫描。

  3. 把这个白色对象赋值给G1栈的黑色对象。

  4. 这个时候把G2对白色对象的引用删掉,这样岂不是会出现黑色白色对象,且为唯一指针?

答案是:这里的关键在于第三步。G1的栈对象接受赋值,这个并不是凭空来的。那么一定是G1自己找来的,可达的对象。这个是一个前提。所以,如果能接受这样的赋值,那么这个白色对象一定是处于G1栈的灰色保护下,因为G1一定是可访问这个对象的。否则,根本就不能完成这个赋值。

混合写屏障的场景,白色对象处于灰色保护下,但是只由堆上的灰色对象保护。注意理解这点;

屏障生成示例:

万字长文图解 Go 内存管理分析:工具、分配和回收原理
  1. 写堆上内容,才会在编译期间生成写屏障

  2. 栈上的写,不会有写屏障。

runtime.gcWriteBarrier :

万字长文图解 Go 内存管理分析:工具、分配和回收原理
  1. 计算出wbBuf的next位置

  2. record ptr

    1. ptr指针放到wbBuf队列中。

  3. *(slot) 存到wbBuf队列中 ( 置灰色,flush了就是灰色 )
    1. shade( *slot )
  4. 如果队列没有满

    1. 那么就赋值写( *(slot) = ptr ); 则返回
  5. 如果队列满了,那么跳到flush

    1. wbBufFlush就是把wbBufFlush里的元属flush到灰色队列中。

    2. 调用完了 runtime.wbBufFlush 处理之后,返回赋值ret( *(slot) = ptr

这么看起来,就不存在 判断stack是否为灰色的条件?

6.8.4 其他屏障


   

writePointer(slot, ptr):

shade(*slot)

shade(ptr)

*slot = ptr

优点:

  1. 这种无条件的屏障更加容易理解,直接把目标和源都置灰色保护

  2. heap上没有黑色到白色的指针

  3. 唯一有可能出现黑色到白色的引用 只可能出现在 被扫描了的stack

  4. 一旦 stack 被扫描过了,只有一种办法能得到白色对象指针(white pointer):通过transfer一个可达(reachable)对象

    1. 删除屏障和混合写屏障,保护了 shade(*slot) 这个指针,就保护了一条路径:这个来路一定是灰色的,下游的白色都会收到保护。并且,我们知道,栈上得到的白色指针一定是可达的,那么一定是有堆上灰色对象保护的。
  5. 任何一个白色对象(被黑色栈对象指向的)一定是被堆上灰色对象保护可达的。

缺点:

这种屏障会导致比较多的屏障,两倍。所以针对这个考虑权衡,会加一个stack条件判断,就是我们看到的混合屏障的样子。

6.9 内存可见性

提一下golang的内存可见性。在c里面,如果是在多线程环境,并发操作一些变量,需要考虑一些可见性的问题。比如赋值一个变量,这个线程还有可能在寄存器里没有刷下去,或者编译器帮你优化到寄存器中,不去内存读。所以有一个volatile关键字,强制去内存读。

golang是否有这个内存可见性的问题?

一句话,golang里面,只要你保证顺序性,那么内存一致性就没有问题。具体可以搜索happen-before的机制。

6.10 注意问题

6.10.1 千万不要尝试绕过golang的类型系统

千万不要尝试绕过golang的类型系统。golang官方在提到uintptr类型的时候,都说不要产生uintptr的临时变量,因为很有可能会导致gc的错误回收(这个做过一个简单的验证,发现新版本的uintptr类型是会作为指针标记的)。

举一个极端的例子,如果你new了一个对象,然后把这个对象的地址保存在8个不连续的byte类型里,那就等着coredump吧。

6.10.2 在golang里按照c的思路实现一个内存池很容易踩到巨坑。

比如现在你分配一个大内存出来(1G的[ ]byte类型空间)。这是一个大内存块。并且golang没有任何标识这个地方标识指针。


   

// 分配一个大内存数组(1GB),数组元素是byte。那么自然每个元素都是不含指针的。

begin := make([]byte, 1024*1024*1024)

万字长文图解 Go 内存管理分析:工具、分配和回收原理

那么扫描是不会扫描这个内部的。

内存池分配器接口: func (ac *Allocator) Alloc (size int) unsafe.Pointer

用来分配对象,使用可能会导致莫名其妙的内存错误。假设用来分配对象T:


   

type T struct {

s *S

}

t := (*T) (ac.Alloc(sizeT))

t.s = &S{}

T对象是从一个大数组里划出来的,垃圾回收其实并不知道T这个对象。不过只要1G内存池本身不被回收,T对象还是安全的。但是T里面的S,是golang走类型系统分配出来的,就会有问题。

假设发生垃圾回收了,GC会认为这个内存空间是一个Byte数组,而不会扫描,那么t.s指向的对象认为未被任何对象引用到,它会被清理掉。最后t.s就成了一个悬挂指针。

golang里面实现内存分配器,适用处理两种情况:

  1. 一种是用于分配对象里面不包含其他引用

  2. 另一种,包含的引用对象也在这个分配器里

其实,没必要自己搞通用内存池。一旦绕过了golang的类型系统,就会出现坑。

推荐阅读

学习交流 Go 语言,扫码回复「 进群 」即可

站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验

Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Mechanics of Web Handling

The Mechanics of Web Handling

David R. Roisum

This unique book covers many aspects of web handling for manufacturing, converting, and printing. The book is applicable to any web including paper, film, foil, nonwovens, and textiles. The Mech......一起来看看 《The Mechanics of Web Handling》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具