“ 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 分析内存占用。
但是不清楚怎么实现?不清楚怎么看指标?不清楚 flat,cum的区别?我们就从这个问题展开。
3. 名字说明
内存分析的时候,有四个输入选项:
-
alloc_objects : 历史总分配的累计
-
alloc_space :历史总分配累计
-
inuse_objects:当前正在使用的对象数
-
堆上分配出来,业务正在使用的,也包括业务没有使用但是还没有垃圾回收掉的对象。
-
inuse_space:当前正在使用的内存
两个输出选项:
-
flat:平坦分配,非累加
-
cum:累加
思考几个问题:
-
上面说的对象是什么概念?
-
经常使用内存分析,这个内存分析是否是精确的?性能消耗大不大
-
为啥显示的是堆栈?不是说分配的对象吗?为啥不直接显示分配的对象结构名?
4. 内存怎么采样?
4.1 编译期间逃逸分析
说明下,golang pprof是分析从堆上分配的内存。golang的内存在堆上,还是在栈上?这个不是我们决定的,就算你调用new这个关键字,也不一定是在堆上分配。
逃逸分析是golang的一个非常重要的一个点。 对于内存分配,垃圾回收的设计都有非常重要的影响。
4.2 采样的简单实现
采样的实现非常简单。简单描述流程:
-
用一个公共变量用来记录
-
分配内存的时候,加alloc size,加alloc对象数
-
释放内存的时候,加free size,加free对象数
累计分配:就是alloc 当前在用 inuse:就是 alloc-free
4.3 内存采样的时机
采样的时机说3个点:
-
分配堆上内存的时候,累计分配
-
回收器释放堆上内存的时候,累计释放
-
每512KB打点采样
但是注意一点:并不是每一次分配内存都会被采样。也就是说这里其实是有个权衡的。现在是每满512KB才会采样一次。这里的考虑是性能和采样效果的权衡。因为采样是要耗费性能的,是要取堆栈的。
怎么理解?举个例子
理想情况下(不考虑其他任何影响):
那么有人会想,这样岂不是会漏掉了很多内存?统计还能用来排查问题吗?
这个是性能和效果的一个考虑,一般来讲,我们是用pprof分析内存占用的时候,在整个golang程序跑起来后,时时刻刻都在分配释放内存,每累计分配512KB,打点一次。虽然会漏掉一些内存分配释放,但是对每个结构都是公平的。如果有一个内存泄露分配行为,那么累计下来一定会被抓住的,并且是非常容易被抓住。
4.4 内存采样的入口
内存采样的入口,这个非常简单理解。肯定是一个在分配内存的函数位置,一个是释放内存的位置。这里要特意提下上下文环境。因为golang是垃圾回收类型的语言,内存分配是完全交由golang自己管理,自己不能管理内存。
两个入口函数:
-
mProf_Malloc
-
mProf_Free
这两个是配套使用的采样打点函数。而且一定是配套的。简单说:
-
mProf_Malloc 是由业务程序行为(赋值器)触发的,分配内存嘛。比如你new了一个对象,这个对象在堆上,那么会调用
mallocgc
分配内存,如果到了采样点,那么会调用mProf_Malloc
采样。 -
mProf_Free 是回收器在确定并且将要回收内存的时候调用的。是垃圾回收过程的一环。并且还要注意一点,只有打过点的(mProf_Malloc计数过的对象,会有一个特殊处理),才会配套使用mProf_Free。
-
不是说,任意给一个内存地址给你。你都知道这个是业务类型。
4.5 内存采样的信息
这里问你的是,golang采样是采样啥?类型信息?这里也说过一点,内存这里和类型系统是没啥关系的。这里采样的是分配栈,也就是分配路径。
4.5.1 flat,cum 分别是怎么来的?
看个例子:
大家可以先猜下,我们看alloc_space。这个内存会是怎么累计到的。实际统计如下:
和大家猜的一样吗?这些是怎么看。
首先说几个结论:
-
flat统计到的,就是这个函数实际分配的。
-
cum是累计的,包含自己分配的,也包含路过的。
-
cum和flat不相同的时候,代表这个函数除了自己分配内存,自己内部调用的别的函数也在分配内存。
重点提示:这个要理解这个,首先要知道,内存采样的是什么,内存采样的是分配栈。
解释说明
(图中140M我们当150M看哈,这里采样少了第一次,细节原因可以看代码,这里提一下,不做阐述。):
-
main函数里,A函数调用了5次,B函数 5次,C函数5次。其中B会调用A,C会调用B。
-
调用一次A会分配10M内存,调用一次B会分配20M,调用一次C会分配30M。总累计分配内存是300M
-
A函数实际调用次数是 15次;这个和flat的值是一致的:150M
-
(A) * 5
-
(B -> A) * 5
-
(C -> B -> A) * 5
-
B函数函数实际调用10次;这个和flat的值也是一致的:100M
-
B * 5
-
(C -> B) * 5
-
C函数5次:这个和flat的值是一致的:50M
-
C * 5
-
main函数300M,也是一致的。
图示
记住一句话:采样是记录分配堆栈,而不是类型信息。
4.6 golang的类型反射
思考几个问题:
-
任意给一个内存地址给你,能知道这个对象类型吗?
-
golang的反射到底是怎么回事?
先说结论:golang里面,内存块是没有携带对象类型信息的,这个跟C是一样的。但是golang又有反射,golang的反射一定要基于interface使用。这个要仔细理解下。
因为,golang里面interface的结构变量,是会记录type类型的。
反射定律一:反射一定是基于接口的。是从接口到反射类型。
反射定律二:反射一定是基于接口的。是从反射类型到接口。
还是那句话,golang的反射一定是依赖接口类型的,一定是经过接口倒腾过的。
因为当前接口这个类型对应了两个内部结构: struct iface
, struct eface
,这两个结构都是会存储type类型。以后的一切都是基于这个类型的。
5. 内存分配
5.1 C语言你分配和释放内存怎么做?
思考一个问题,在C语言里,我们分配内存:
分配内存的时候,传入大小,拿到一个指针。
ptr = malloc(1024);
释放内存的时候,直接传入ptr,没有任何其他参数:
free (ptr);
释放的时候,怎么确定释放哪些位置?如果要你自己实现,有很多简单的思路,说一个最简单的:分配的时候,不止分配1024字节,还分配了其他的信息,带head了。
这种分配方式有什么问题:
-
开销大,在通用的内存分配器中,很多场景下,有可能meta信息比自身还要大。
5.2 内存分配设计考虑的几个问题
-
性能
-
局部性
-
碎片率
-
内部碎片率
-
外部碎片率
5.3 golang的内存分配
golang大方向的考虑就是基于局部性和碎片率来考虑的。使用的是和tcmalloc一致的设计。
5.3.1 整体设计
首先,内存块是不带类型信息的。像我们在C语言里面,有时候实现的简单的内存池,在不考虑一些开销的时候,会把业务类型放到meta信息里,为的是排查问题方便。golang内存管理作为一个通用模块,不会这么搞。
5.3.1.1 地址空间设计
很多时候,你查golang的资料,会看到这张图:
这张图有几个信息比较重要:
-
为什么spans区域是512M,bitmap区是16G,arena是512G?先不要纠结值,我们先说这个比例关系:
-
spans区域,一个指针大小(8Byte)对应arena的一个page(8KB),倍数是1024
-
bitmap区域,一个字节(8bit)对应arena的32Bytes,倍数是32倍
-
我们给用户分配的内存就是arena区域的内存,spans区,bitmap区均为其他用途的元数据信息。
-
bitmap这个实现我们这次不谈,不同通过这个你得知道一点:并不是所有的内存空间都会扫描一把,是有挑选判断的。
-
spans区域是一般用来根据一个内存地址查询mspan结构的。调用函数:spanOf。
-
bitmap是用来辅助垃圾回收用的区域。有这个bitmap信息可以提高回收效率和精度。注意一点,这个不是标识object是否分配的位图,标识是否分配object的问题是
mspan.allocBits
结构。这个可以理解为提高垃圾回收效率的实现。
注意几个点:
-
很多文章都提到golang内存512GB这个事情。512GB说的是内存虚拟地址空间的限制,是最大能力,是最大的规划利用。golang之前最大可以使用的内存地址空间。
-
golang1.11 之后已经没有512GB的限制了。基本上和系统的虚拟地址空间一致
-
这个比例还是一样的,1:1024,1:32
-
就算golang1.11之前,也不是说golang的程序上来就向系统申请这么大块虚拟地址。也是每64M的申请,管理对象单元是heapArea结构。
-
三个区域看着连续结在一起,但是其实不是连续的地址。
-
实际的实现中都是以64M(heapArena)的小单位进行的。
5.3.2 抽象对象概念
物理偏向概念:
-
heapArena:堆上物理空间管理的一个小单元,64M一个。
-
page:物理内存最小单位,8KB一个。
逻辑偏向概念:
-
span:span为内存分配的一个管理单元。span内按照固定大小size划分,相同的size划分为同一类。一个span管理一个连续的page。
-
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%左右。
说明:
-
tail wast实际是浪费的外部碎片
-
比如说,第一种size,8字节。一个page 8KB,8字节刚好对齐。外部碎片为0.
-
max waste说的是最大的内部碎片率
-
怎么算的?每一个放进该span的对象大小都是最小值的情况
-
比如说,第一种size,8字节。最小的对象是1字节,浪费7字节,最大碎片率为 1-1/8 = 87.5%
怎么的出来的这些值?经验值吧,可能。
6. 内存回收
6.1 golang协程抢占执行
首先,golang没有真正的抢占。golang调度单位为协程,所谓抢占,也就是强行剥夺执行权。但是有一点,golang本质上是非抢占的,不像操作系统那样,有时钟中断和时间片的概念。golang虽然里面是有一个抢占的概念,但是注意了,这个抢占是建议性质的抢占,也就是说,如果有协程不听话,那是没有办法的,实现抢占的效果是要对方协程自己配合的。
一句话:系统想让某个goroutine自己放弃执行权,会给这个协程设置一个魔数,协程在切调度,或者其他时机检查到了的时候,会感知到这一个行为。
当前的抢占实现是:
-
给这个协程设置一个的魔数(stackguard)。每个函数的入口会比较当前栈寄存器值和stackguard值来决定是否触发morestack函数。(这是一个抢占调度点)
-
协程调用函数的时候,会检查是否需要栈扩容。如果被设置了抢占标示,那么就会首先调用到
-
调用newstack,在newstack里面判断是否是特殊值,这种特殊值,目的不在于扩容,而在于让出调度。
所以,在golang里面,只要有函数调用,就会有感知抢占的时机。stw就是基于这个实现的。
思考一个问题:
如果有一个猥琐的函数:非常耗时,一直在做cpu操作,并且完全没有函数调用。这种情况下,golang是没有一点办法的。那么这种情况会影响到整个程序的能力。
所以,我们平时写函数,一定要短小精悍,功能拆分合理。
6.2 STW是怎么回事?
STW:stop the world,也就是说暂停说由协程的调度和执行。stw是怎么实现?stw的基础就是上面提到的抢占实现。stw调用的目的是为了让整个程序(赋值器停止),那么就需要剥夺每一个协程的执行。
stw在垃圾回收的几个关键操作里是需要的,比如开启垃圾回收,需要stw,做好准备工作。如果stw的时候,出现了猥琐的函数,那么会导致整个系统的能力降低。因为大家都在等你一个人。
6.3 垃圾回收要求
-
正确性:绝对不能回收正在使用的的内存对象。
-
存活性:一轮回收过程一定是有边界,可结束的。
6.4 golang版本迭代历史
-
go 1.3 以前,使用是标记-清扫的方式,整个过程需要stw
-
go 1.3 版本分离了标记和清扫操作,标记过程stw,清扫过程并发执行
-
go 1.5 版本在标记过程中,使用三色标记法。回收过程分为四个阶段,其中,标记和清扫都并发执行的,但标记阶段的前后需要stw一定时间来做gc的准备工作和栈的re-scan。
-
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之前的图:
当下GC大概分为四个阶段:
-
GC准备阶段
-
标记阶段
-
标记结束阶段
-
清理阶段
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:堆上对象赋值的时候,插入写屏障,保护强三色不变式
step2:删除的时候,没啥问题
栈上对象赋值:
step3:栈上对象赋值的时候,没有写屏障。白色对象直接被黑色对象引用。
step4:删除灰色保护路径。
所以才需要在mark terminato阶段,重新扫描栈。
6.8.2 删除写屏障
(Yuasa '90)
writePointer ( slot, ptr ):
// 删除之前,保护原先白色或者灰色指向的数据块
if ( isGery ( slot ) || isWhite ( slot ) )
shade ( *slot )
*slot = ptr
这个是通用的一种写屏障技术。golang并没有实现,而是实现了插入写屏障。原因就在于:这个在垃圾回收之前,必须做一个快照扫描,这个就会对用户时延有比较严重的影响。下面详述。
主要流程:
-
在标记之前,需要打一个引用关系的快照。所以,这个对于栈内存很大的时候,影响越大。
-
不需要完整的快照,只需要在扫描堆对象之前,确保所有的栈对象是黑色的。引用都是灰色的,这样就保证了一个前提:所有可达的对象都处于灰色保护状态中。
-
对栈快照扫描需要stw,去扫描栈对象。这个时候,是需要暂停所有的用户程序。
-
扫描堆对象的时候,可以和应用程序并发的。此后根一直保持黑色(黑色赋值器),不用再扫描栈。
-
对象被删除的时候,删除写屏障会捕捉到。置灰。
-
上面的伪代码显示有条件,其实第一版的时候是没有条件的。
-
这里加上条件是为了回收精度:当上游之前是白色或者灰色才需要把这个置灰色。如果是黑?那么一定是处于灰色保护状态,因为这个是前提(理解这个非常重要)。
(开始的时候,stw扫描栈,得到灰色对象)
图表演示
初始扫描快照后:
step1: 赋值。这里赋值是允许的,虽然是破坏了强三色不变式。但是还是符合弱三色不变式。
step2:删除。这里就拦截了,必须置灰色。保证弱三色不变式。
回收精度:
删除写屏障的精度比插入写屏障的精度更低。删除的即使是最后一个指针,也会保留到下一轮,属于一个浮动垃圾。这个比插入屏障精度还低。因为,对于插入屏障所保留的对象,回收器至少可以确定曾在其中执行了某些回收相关的操作(获取或写入对象的引用),但删除屏障所保留的对象却不一定被赋值器操作过。
为什么需要打快照?
删除写屏障,又叫快照屏障增量技术(或者说,一定要配合这个来做)。
-
首先,是需要stw,针对扫描整个栈根打做一遍扫描。相当于一个快照。这个过程扫描之后,就能保证当前(时刻)所有可达的对象都处于灰色保护状态,满足弱三色不变式。
-
然后,赋值器和回收器就可以并发。但是并发有可能会破坏导致弱三色不变式。这个时候,就需要删除写屏障来时刻保护白色对象。
golang为啥没有用这个?
-
一个是精度问题,这个精度要比插入写屏障低;
-
考虑goroutine可能非常多,不适合上来就stw,扫描所有的内存栈。这个适合小内存的场景。
-
思考一个问题:这个和混合写屏障有没有区别?还是有区别的,这里是要锁整个栈,混合写屏障是并发的,每次只需要锁单个栈。
6.8.3 混合写屏障
混合屏障是结合插入屏障和删除屏障。
伪代码:
writePointer (slot, ptr) :
// 保护原来的(被删除的)
shade ( *slot )
if current stack is grey:
// 如果对象为灰色,则还需要保护新指向的对象
shade ( ptr )
*slot = ptr
(开始的时候,stw扫描栈,得到黑色对象)
golang实际情况:
伪代码如上。但是这里提出来一点,golang根本不是和伪代码说的这样。没有做条件判断,所以现在的回收精度很低。这个算是一个TodoList。
注意:使用了混合屏障,还是针对堆上的,栈上对象写入还是没有barrier。golang之前只使用插入屏障,关键在于栈对象没有,导致栈上黑对象可能指向白对象。所以要rescan。因为如果不rescan,而且又破坏了弱三色不变式(没有处于灰色保护链中),那么就丢数据了。
混合屏障,就是结合删除屏障,保护这一个前提,代价就是进一步降低回收精度。
图表示例:
混合屏障就是要解决: 栈指向白色对象,stw重新扫描栈的问题。
step1:赋值白对象到黑对象引用,这个不会阻止这个,也不会有写屏障。就是一个正常的赋值。
-
这个时候黑色指向了白色对象。破坏了强三色不变式。
-
但是这个白色对象还处于灰色状态保护下。符合弱三色不变式。
step2:删除指针的时候,意图破坏弱三色不变式的时候,写屏障就会把这个对象置灰色。
问题一:如果有个还会想?由于栈上没有写屏障,这个删除的对象式根指向的呢?如果存在以下场景?
step1:堆上的白色对象引用赋值给黑色栈对象。
step2:如果删除指针,岂不是连弱三色不变式也破坏了?
这个怎么办呢?
答案是:其实根本就不可能出现这个场景的引用图。第一个图就不会出现。因为虽然没有stw,但是扫描某个g的时候,这个g是暂停的。相当于这个g栈是一个快照状态。
混合写屏障的栈,要么全黑,要么全白(单个栈)
那么这个暂停g这个是怎么做到的?
-
扫描的时候,会设置一个 _Gscan 状态。
-
casgstatus的时候,保证循环等待这个状态完成。之前是直接吃cpu的,后面做了一个优化,加了一个yield,5us的间隔。
-
关于这段代码的改动
-
问题二:如果是多个栈呢,那么就不是原子的快照了。比如下图?那么就可能导致这种情况。
如果说A和前面的黑色对象不属于同一个g栈。那么是否可能会导致这种场景出现?分析下:
-
这个场景是有这么一个白色对象,先只被G2栈根引用到。
-
当前G1已经被扫描完,G2还没有扫描。
-
把这个白色对象赋值给G1栈的黑色对象。
-
这个时候把G2对白色对象的引用删掉,这样岂不是会出现黑色白色对象,且为唯一指针?
答案是:这里的关键在于第三步。G1的栈对象接受赋值,这个并不是凭空来的。那么一定是G1自己找来的,可达的对象。这个是一个前提。所以,如果能接受这样的赋值,那么这个白色对象一定是处于G1栈的灰色保护下,因为G1一定是可访问这个对象的。否则,根本就不能完成这个赋值。
混合写屏障的场景,白色对象处于灰色保护下,但是只由堆上的灰色对象保护。注意理解这点;
屏障生成示例:
-
写堆上内容,才会在编译期间生成写屏障
-
栈上的写,不会有写屏障。
runtime.gcWriteBarrier :
-
计算出wbBuf的next位置
-
record ptr
-
ptr指针放到wbBuf队列中。
-
把
*(slot)
存到wbBuf队列中 ( 置灰色,flush了就是灰色 ) -
shade( *slot )
-
如果队列没有满
-
那么就赋值写(
*(slot) = ptr
); 则返回 -
如果队列满了,那么跳到flush
-
wbBufFlush就是把wbBufFlush里的元属flush到灰色队列中。
-
调用完了 runtime.wbBufFlush 处理之后,返回赋值ret(
*(slot) = ptr
)
这么看起来,就不存在 判断stack是否为灰色的条件?
6.8.4 其他屏障
writePointer(slot, ptr):
shade(*slot)
shade(ptr)
*slot = ptr
优点:
-
这种无条件的屏障更加容易理解,直接把目标和源都置灰色保护
-
heap上没有黑色到白色的指针
-
唯一有可能出现黑色到白色的引用 只可能出现在 被扫描了的stack
-
一旦 stack 被扫描过了,只有一种办法能得到白色对象指针(white pointer):通过transfer一个可达(reachable)对象
-
删除屏障和混合写屏障,保护了
shade(*slot)
这个指针,就保护了一条路径:这个来路一定是灰色的,下游的白色都会收到保护。并且,我们知道,栈上得到的白色指针一定是可达的,那么一定是有堆上灰色对象保护的。 -
任何一个白色对象(被黑色栈对象指向的)一定是被堆上灰色对象保护可达的。
缺点:
这种屏障会导致比较多的屏障,两倍。所以针对这个考虑权衡,会加一个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)
那么扫描是不会扫描这个内部的。
内存池分配器接口: 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里面实现内存分配器,适用处理两种情况:
-
一种是用于分配对象里面不包含其他引用
-
另一种,包含的引用对象也在这个分配器里
其实,没必要自己搞通用内存池。一旦绕过了golang的类型系统,就会出现坑。
推荐阅读
学习交流 Go 语言,扫码回复「 进群 」即可
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 一篇长文学懂 pytorch
- Redux 源码解读(长文慎入)
- 移动 Web 最佳实践(干货长文,建议收藏)
- 万字长文拿下HTTPS,面试不再慌!
- 万字长文聊缓存(下):应用级缓存
- 万字长文:Kubernetes operator 模式开发实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。