本文目錄一覽:
- 1、(十一)golang 內存分析
- 2、golang是自動釋放內存嗎
- 3、每天一個知識點:Golang 內存逃逸
- 4、Golang 1.14中內存分配、清掃和內存回收
- 5、golang內存擴容
- 6、Golang實驗性功能SetMaxHeap 固定值GC
(十一)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 功能,可能有些描述與實現細節不完全一致,如有錯誤還請指出
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/188932.html