本文目录一览:
(十一)golang 内存分析
编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2。 除了glibc,业界比较出名的内存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免内存碎片和性能上均比glic有比较大的优势,在多线程环境中效果更明显。
Golang中也实现了内存分配器,原理与tcmalloc类似,简单的说就是维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存,私有内存不足再从全局申请。另外,内存分配与GC(垃圾回收)关系密切,所以了解GC前有必要了解内存分配的原理。
为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存。 以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示:
预申请的内存划分为spans、bitmap、arena三部分。其中arena即为所谓的堆区,应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。
arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;
spans区域存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)乘以指针大小8byte = 512M
bitmap区域大小也是通过arena计算出来,不过主要用于GC。
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。
根据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小。如下表所示:
上表中每列含义如下:
class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
bytes/obj:该class代表对象的字节数
bytes/span:每个span占用堆的字节数,也即页数乘以页大小
objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)waste
bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)上表可见最大的对象是32K大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。
span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小,span将一个或多个页拆分成多个块进行管理。src/runtime/mheap.go:mspan定义了其数据结构:
以class 10为例,span和管理的内存如下图所示:
spanclass为10,参照class表可得出npages=1,nelems=56,elemsize为144。其中startAddr是在span初始化时就指定了某个页的地址。allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配,其allocCount也为2。next和prev用于将多个span链接起来,这有利于管理多个span,接下来会进行说明。
有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache。src/runtime/mcache.go:mcache定义了cache的数据结构
alloc为mspan的指针数组,数组大小为class总数的2倍。数组中每个元素代表了一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要GC进行扫描。mcache和span的对应关系如下图所示:
mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况,每种class的span个数也不相同。上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central。src/runtime/mcentral.go:mcentral定义了central数据结构:
lock: 线程间互斥锁,防止多线程读写冲突
spanclass : 每个mcentral管理着一组有相同class的span列表
nonempty: 指还有内存可用的span列表
empty: 指没有内存可用的span列表
nmalloc: 指累计分配的对象个数线程从central获取span步骤如下:
将span归还步骤如下:
从mcentral数据结构可见,每个mcentral对象只管理特定的class规格的span。事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中。src/runtime/mheap.go:mheap定义了heap的数据结构:
lock: 互斥锁
spans: 指向spans区域,用于映射span和page的关系
bitmap:bitmap的起始地址
arena_start: arena区域首地址
arena_used: 当前arena已使用区域的最大地址
central: 每种class对应的两个mcentral
从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。mheap内存管理示意图如下:
系统预分配的内存分为spans、bitmap、arean三个区域,通过mheap管理起来。接下来看内存分配过程。
针对待分配对象的大小不同有不同的分配逻辑:
(0, 16B) 且不包含指针的对象: Tiny分配
(0, 16B) 包含指针的对象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴,这里暂时仅关注一般的分配方法。
以申请size为n的内存为例,分配步骤如下:
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。1、Golang程序启动时申请一大块内存并划分成spans、bitmap、arena区域
2、arena区域按页划分成一个个小块。
3、span管理一个或多个页。
4、mcentral管理多个span供线程申请使用
5、mcache作为线程私有资源,资源来源于mcentral。
golang是自动释放内存吗
golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。
1.内存池:
应该有一个主要管理内存分配的部分,向系统申请大块内存,然后进行管理和分配。
2.垃圾回收:
当分配的内存使用完之后,不直接归还给系统,而是归还给内存池,方便进行下一次复用。至于垃圾回收选择标记回收,还是分代回收算法应该符合语言设计初衷吧。
3.大小切分:
使用单独的数组或者链表,把需要申请的内存大小向上取整,直接从这个数组或链表拿出对应的大小内存块,方便分配内存。大的对象以页申请内存,小的对象以块来申请,避免内存碎片,提高内存使用率。
4.多线程管理:
每个线程应该有自己的内存块,这样避免同时访问共享区的时候加锁,提升语言的并发性,线程之间通信使用消息队列的形式,一定不要使用共享内存的方式。提供全局性的分配链,如果线程内存不够用了,可向分配链申请内存。
这样的内存分配设计涵盖了大部分语言的,上面的想法其实是把golang语言内存分配抽象出来。其实Java语言也可以以同样的方式理解。内存池就是JVM堆,主要负责申请大块内存;多线程管理方面是使用栈内存,每个线程有自己独立的栈内存进行管理。
golang内存分配器
golang内存分配器主要包含三个数据结构:MHeap,MCentral以及MCache
1.MHeap:分配堆,主要是负责向系统申请大块的内存,为下层MCentral和MCache提供内存服务。他管理的基本单位是MSpan(若干连续内存页的数据结构)
type MSpan struct
{
MSpan *next;
MSpan *prev;
PageId start; // 开始的页号
uintptr npages; // 页数
…..
};
可以看出MSpan是一个双端链表的形式,里面存储了它的一些位置信息。
通过一个基地址+(页号*页大小),就可以定位到这个MSpan的实际内存空间。
type MHeap struct
{
lock mutex;
free [_MaxMHeapList] mSpanList // free lists of given length
freelarge mSpanList // free lists length = _MaxMHeapList
busy [_MaxMHeapList] mSpanList // busy lists of large objects of given length
busylarge mSpanList
};
free数组以span为序号管理多个链表。当central需要时,只需从free找到页数合适的链表。large链表用于保存所有超出free和busy页数限制的MSpan。
MHeap示意图:
2.MCache:运行时分配池,不针对全局,而是每个线程都有自己的局部内存缓存MCache,他是实现goroutine高并发的重要因素,因为分配小对象可直接从MCache中分配,不用加锁,提升了并发效率。
type MCache struct
{
tiny byte*; // Allocator cache for tiny objects w/o pointers.
tinysize uintptr;
alloc[NumSizeClasses] MSpan*; // spans to allocate from
};
尽可能将微小对象组合到一个tiny块中,提高性能。
alloc[]用于分配对象,如果没有了,则可以向对应的MCentral获取新的Span进行操作。
线程中分配小对象(16~32K)的过程:
对于
size 介于 16 ~ 32K byte 的内存分配先计算应该分配的 sizeclass,然后去 mcache 里面
alloc[sizeclass] 申请,如果 mcache.alloc[sizeclass] 不足以申请,则 mcache 向 mcentral
申请mcentral 给 mcache 分配完之后会判断自己需不需要扩充,如果需要则想 mheap 申请。
每个线程内申请内存是逐级向上的,首先看MCache是否有足够空间,没有就像MCentral申请,再没有就像MHeap,MHeap向系统申请内存空间。
3.MCentral:作为MHeap和MCache的承上启下的连接。承上,从MHeap申请MSpan;启下,将MSpan划分为各种尺寸的对象提供给MCache使用。
type MCentral struct
{
lock mutex;
sizeClass int32;
noempty mSpanList;
empty mSpanList;
int32 nfree;
……
};
type mSpanList struct {
first *mSpan
last *mSpan
};
sizeclass: 也有成员 sizeclass,用于将MSpan进行切分。
lock: 因为会有多个 P 过来竞争。
nonempty: mspan 的双向链表,当前 mcentral 中可用的 mSpan list。
empty: 已经被使用的,可以认为是一种对所有 mSpan 的 track。MCentral存在于MHeap内。
给对象 object 分配内存的主要流程:
1.object size 32K,则使用 mheap 直接分配。
2.object size 16 byte,使用 mcache 的小对象分配器 tiny 直接分配。 (其实 tiny 就是一个指针,暂且这么说吧。)
3.object size 16 byte size =32K byte 时,先使用 mcache 中对应的 size class 分配。
4.如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。
5.如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。
6.如果 mheap 也没有合适的 span,则想操作系统申请。
tcmalloc内存分配器介绍
tcmalloc(thread-caching mallo)是google推出的一种内存分配器。
具体策略:全局缓存堆和进程的私有缓存。
1.对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。
2.对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。
golang语言中MHeap就是全局缓存堆,MCache作为线程私有缓存。
在文章开头说过,内存池就是利用MHeap实现,大小切分则是在申请内存的时候就做了,同时MCache分配内存时,可以用MCentral去取对应的sizeClass,多线程管理方面则是通过MCache去实现。
总结:
1.MHeap是一个全局变量,负责向系统申请内存,mallocinit()函数进行初始化。如果分配内存对象大于32K直接向MHeap申请。
2.MCache线程级别管理内存池,关联结构体P,主要是负责线程内部内存申请。
3.MCentral连接MHeap与MCache的,MCache内存不够则向MCentral申请,MCentral不够时向MHeap申请内存。
每天一个知识点:Golang 内存逃逸
在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式,寻址起来十分迅速,开销很少。这一块内存地址称为栈。栈是线程级别的,大小在创建的时候已经确定,当变量太大的时候,会"逃逸"到堆上,这种现象称为内存逃逸。简单来说,局部变量通过堆分配和回收,就叫内存逃逸。
如果一个函数返回对一个变量的引用,那么它就会发生逃逸。即任何时候,一个值被分享到函数栈范围之外,它都会在堆上被重新分配。在这里有一个例外,就是如果编译器可以证明在函数返回后不会再被引用的,那么就会分配到栈上,这个证明的过程叫做逃逸分析。
堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。变量在堆上的分配和回收都比在栈上开销大的多。对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片(采用分区式存储管理的系统,在储存分配过程中产生的、不能供用户作业使用的主存里的小分区称成“内存碎片”。内存碎片分为内部碎片和外部碎片)。
简单聊聊内存逃逸 | 剑指offer - golang
Golang内存逃逸是什么?怎么避免内存逃逸?
Golang 1.14中内存分配、清扫和内存回收
Golang的内存分配是由golang runtime完成,其内存分配方案借鉴自tcmalloc。
主要特点就是
本文中的element指一定大小的内存块是内存分配的概念,并为出现在golang runtime源码中
本文讲述x8664架构下的内存分配
Golang 内存分配有下面几个主要结构
Tiny对象是指内存尺寸小于16B的对象,这类对象的分配使用mcache的tiny区域进行分配。当tiny区域空间耗尽时刻,它会从mcache.alloc[tinySpanClass]指向的mspan中找到空闲的区域。当然如果mcache中span空间也耗尽,它会触发从mcentral补充mspan到mcache的流程。
小对象是指对象尺寸在(16B,32KB]之间的对象,这类对象的分配原则是:
1、首先根据对象尺寸将对象归为某个SpanClass上,这个SpanClass上所有的element都是一个统一的尺寸。
2、从mcache.alloc[SpanClass]找到mspan,看看有无空闲的element,如果有分配成功。如果没有继续。
3、从mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合适的mspan,返回给mcache。如果没有找到就进入mcentral.grow()—mheap.alloc()分配新的mspan给mcentral。
大对象指尺寸超出32KB的对象,此时直接从mheap中分配,不会走mcache和mcentral,直接走mheap.alloc()分配一个SpanClass==0 的mspan表示这部分分配空间。
对于程序分配常用的tiny和小对象的分配,可以通过无锁的mcache提升分配性能。mcache不足时刻会拿mcentral的锁,然后从mcentral中充mspan 给mcache。大对象直接从mheap 中分配。
在x8664环境上,golang管理的有效的程序虚拟地址空间实质上只有48位。在mheap中有一个pages pageAlloc成员用于管理golang堆内存的地址空间。golang从os中申请地址空间给自己管理,地址空间申请下来以后,golang会将地址空间根据实际使用情况标记为free或者alloc。如果地址空间被分配给mspan或大对象后,那么被标记为alloc,反之就是free。
Golang认为地址空间有以下4种状态:
Golang同时定义了下面几个地址空间操作函数:
在mheap结构中,有一个名为pages成员,它用于golang 堆使用虚拟地址空间进行管理。其类型为pageAlloc
pageAlloc 结构表示的golang 堆的所有地址空间。其中最重要的成员有两个:
在golang的gc流程中会将未使用的对象标记为未使用,但是这些对象所使用的地址空间并未交还给os。地址空间的申请和释放都是以golang的page为单位(实际以chunk为单位)进行的。sweep的最终结果只是将某个地址空间标记可被分配,并未真正释放地址空间给os,真正释放是后文的scavenge过程。
在gc mark结束以后会使用sweep()去尝试free一个span;在mheap.alloc 申请mspan时刻,也使用sweep去清扫一下。
清扫mspan主要涉及到下面函数
如上节所述,sweep只是将page标记为可分配,但是并未把地址空间释放;真正的地址空间释放是scavenge过程。
真正的scavenge是由pageAlloc.scavenge()—sysUnused()将扫描到待释放的chunk所表示的地址空间释放掉(使用sysUnused()将地址空间还给os)
golang的scavenge过程有两种:
golang内存扩容
一般来说当内存空间span不足时,需要进行扩容。而在扩容前需要将当前没有剩余空间的内存块相关状态解除,以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中。
需要注意由于中间部件有scan和noscan两种类型,则申请的内存空间最终获取的可能是其两倍,并由heap堆进行统一管理。中间部件central是通过两个链表来管理其分配的所有内存块:
1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块
2、noempty代表剩余的空间,并这些内存块能够提供服务
由于golang垃圾回收器使用的累增计数器(heap.sweepgen)来表达代龄的:
从上面内容可以看到每次进行清理操作时 该计数器 +2
再来看下mcentral的构成
当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块,这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为了获取更多的可用空间,则会在将分配的内存块移交给cache部件前,先完成清理的操作。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了)。第三步若是noempty和empty都没有申请到,这时需要堆进行申请内存的
通过上面的源码也可以看到中间部件central自身扩容操作与大对象内存分配差不多类似。
在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储,这样能够有效的减少内存浪费。
当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存,若容量不足,则重新提取一块;前面也提到会将微小对象进行组合,而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因,一般都是当前存储单元里所有的微小对象都不可达时,才会将该块内存进行回收。
而当从缓冲部件cache中获取空间span时, 是通过偏移位置(tinyoffset)先来判断剩余空间是否满足需求。若是可以的话则以此计算并返回内存地址;若是空间不足,则提取新的内存块,直接返回起始地址便可; 最后在对比新旧两块内存,空间大的那块则会被保留。
Golang实验性功能SetMaxHeap 固定值GC
简单来说, SetMaxHeap 提供了一种可以设置固定触发阈值的 GC (Garbage Collection垃圾回收)方式
官方源码链接
大量临时对象分配导致的 GC 触发频率过高, GC 后实际存活的对象较少,
或者机器内存较充足,希望使用剩余内存,降低 GC 频率的场景
GC 会 STW ( Stop The World ),对于时延敏感场景,在一个周期内连续触发两轮 GC ,那么 STW 和 GC 占用的 CPU 资源都会造成很大的影响, SetMaxHeap 并不一定是完美的,在某些场景下做了些权衡,官方也在进行相关的实验,当前方案仍没有合入主版本。
先看下如果没有 SetMaxHeap ,对于如上所述的场景的解决方案
这里简单说下 GC 的几个值的含义,可通过 GODEBUG=gctrace=1 获得如下数据
这里只关注 128-132-67 MB 135 MB goal ,
分别为 GC开始时内存使用量 - GC标记完成时内存使用量 - GC标记完成时的存活内存量 本轮GC标记完成时的 预期 内存使用量(上一轮 GC 完成时确定)
引用 GC peace设计文档 中的一张图来说明
对应关系如下:
简单说下 GC pacing (信用机制)
GC pacing 有两个目标,
那么当一轮 GC 完成时,如何只根据本轮 GC 存活量去实现这两个小目标呢?
这里实际是根据当前的一些数据或状态去 预估 “未来”,所有会存在些误差
首先确定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 为本轮 GC 存活量, gcpercent 默认为 100 ,可以通过环境变量 GOGC=100 或者 debug.SetGCPercent(100) 来设置
那么默认情况下 goal = 2 * heap_marked
gc_trigger 是与 goal 相关的一个值( gc_trigger 大约为 goal 的 90% 左右),每轮 GC 标记完成时,会根据 |Ha-Hg| 和实际使用的 cpu 资源 动态调整 gc_trigger 与 goal 的差值
goal 与 gc_trigger 的差值即为,为 GC 期间分配的对象所预留的空间
GC pacing 还会预估下一轮 GC 发生时,需要扫描对象对象的总量,进而换算为下一轮 GC 所需的工作量,进而计算出 mark assist 的值
本轮 GC 触发( gc_trigger ),到本轮的 goal 期间,需要尽力完成 GC mark 标记操作,所以当 GC 期间,某个 goroutine 分配大量内存时,就会被拉去做 mark assist 工作,先进行 GC mark 标记赚取足够的信用值后,才能分配对应大小的对象
根据本轮 GC 存活的内存量( heap_marked )和下一轮 GC 触发的阈值( gc_trigger )计算 sweep assist 的值,本轮 GC 完成,到下一轮 GC 触发( gc_trigger )时,需要尽力完成 sweep 清扫操作
预估下一轮 GC 所需的工作量的方式如下:
继续分析文章开头的问题,如何充分利用剩余内存,降低 GC 频率和 GC 对 CPU 的资源消耗
如上图可以看出, GC 后,存活的对象为 2GB 左右,如果将 gcpercent 设置为 400 ,那么就可以将下一轮 GC 触发阈值提升到 10GB 左右
前面一轮看起来很好,提升了 GC 触发的阈值到 10GB ,但是如果某一轮 GC 后的存活对象到达 2.5GB 的时候,那么下一轮 GC 触发的阈值,将会超过内存阈值,造成 OOM ( Out of Memory ),进而导致程序崩溃。
可以通过 GOGC=off 或者 debug.SetGCPercent(-1) 来关闭 GC
可以通过进程外监控内存使用状态,使用信号触发的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式进行堆内存使用的监测
可以通过调用 runtime.GC() 或者 debug.FreeOSMemory() 来手动进行 GC 。
这里还需要说几个事情来解释这个方案所存在的问题
通过 GOGC=off 或者 debug.SetGCPercent(-1) 是如何关闭 GC 的?
gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)
通过 GC trace 可以看出,上面所说的 goal 变成了一个很诡异的值 17592186044415
实际上关闭 GC 后, Go 会将 goal 设置为一个极大值 ^uint64(0) ,那么对应的 GC 触发阈值也被调成了一个极大值,这种处理方式看起来也没什么问题,将阈值调大,预期永远不会再触发 GC
那么如果在关闭 GC 的情况下,手动调用 runtime.GC() 会导致什么呢?
由于 goal 和 gc_trigger 被设置成了极大值, mark assist 和 sweep assist 也会按照这个错误的值去计算,导致工作量预估错误,这一点可以从 trace 中进行证明
可以看到很诡异的 trace 图,这里不做深究,该方案与 GC pacing 信用机制不兼容
记住,不要在关闭 GC 的情况下手动触发 GC ,至少在当前 Go1.14 版本中仍存在这个问题
SetMaxHeap 的实现原理,简单来说是强行控制了 goal 的值
注: SetMaxHeap ,本质上是一个软限制,并不能解决 极端场景 下的 OOM ,可以配合内存监控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆内存大小, Go 中除了堆内存还分配了如下内存,所以实际使用过程中,与实际硬件内存阈值之间需要留有一部分余量。
对于文章开始所述问题,使用 SetMaxHeap 后,预期的 GC 过程大概是这个样子
简单用法1
该方法简单粗暴,直接将 goal 设置为了固定值
注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal
简单用法2
当不关闭 GC 时, SetMaxHeap 的逻辑是, goal 仍按照 gcpercent 进行计算,当 goal 小于 SetMaxHeap 阈值时不进行处理;当 goal 大于 SetMaxHeap 阈值时,将 goal 限制为 SetMaxHeap 阈值
注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal
切换到 go1.14 分支,作者选择了 git checkout go1.14.5
选择官方提供的 cherry-pick 方式(可能需要梯子,文件改动不多,我后面会列出具体改动)
git fetch "" refs/changes/67/227767/3 git cherry-pick FETCH_HEAD
需要重新编译Go源码
注意点:
下面源码中的官方注释说的比较清楚,在一些关键位置加入了中文注释
入参bytes为要设置的阈值
notify 简单理解为 GC 的策略 发生变化时会向 channel 发送通知,后续源码可以看出“策略”具体指哪些内容
返回值为本次设置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
注:作者尽量用通俗易懂的语言去解释 Go 的一些机制和 SetMaxHeap 功能,可能有些描述与实现细节不完全一致,如有错误还请指出