<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊兩個Go即將過時的GC優(yōu)化策略

          共 19070字,需瀏覽 39分鐘

           ·

          2022-07-04 17:46

          這篇文章本來是要講 Go Memory Ballast 以及 Go GC Tuner 來調整 GC 的策略,實現(xiàn)原理怎么樣,效果如何。但是在寫的過程中,發(fā)現(xiàn) Go 1.19版本出了,有個新特性讓這兩個優(yōu)化終究成為歷史。

          概述

          首先我們來簡單的看一下 Go GC中做了什么事,以及它里面比較耗時的地方是什么,我們才能對它進行優(yōu)化。

          首先對于 GC 來說有這么幾個階段:

          1. 1. sweep termination(清理終止):會觸發(fā) STW ,所有的 P(處理器) 都會進入 safe-point(安全點);

          2. 2. the mark phase(標記階段):恢復程序執(zhí)行,GC 執(zhí)行根節(jié)點的標記,這包括掃描所有的棧、全局對象以及不在堆中的運行時數(shù)據(jù)結構;

          3. 3. mark termination(標記終止):觸發(fā) STW,扭轉 GC 狀態(tài),關閉 GC 工作線程等;

          4. 4. the sweep phase(清理階段):恢復程序執(zhí)行,后臺并發(fā)清理所有的內存管理單元;

          在這幾個階段中,由于標記階段是要從根節(jié)點對堆進行遍歷,對存活的對象進行著色標記,因此標記的時間和目前存活的對象有關,而不是與堆的大小有關,也就是堆上的垃圾對象并不會增加 GC 的標記時間。

          并且對于現(xiàn)代操作系統(tǒng)來說釋放內存是一個非常快的操作,所以 Go 的 GC 時間很大程度上是由標記階段決定的,而不是清理階段。

          在什么時候會觸發(fā) GC ?

          我在這篇文章 https://www.luozhiyun.com/archives/475 做源碼分析的時候有詳細的講到過,我這里就簡單的說下。

          在 Go 中主要會在三個地方觸發(fā) GC:

          1、監(jiān)控線程 runtime.sysmon 定時調用;

          2、手動調用 runtime.GC 函數(shù)進行垃圾收集;

          3、申請內存時 runtime.mallocgc 會根據(jù)堆大小判斷是否調用;

          runtime.sysmon

          Go 程序在啟動的時候會后臺運行一個線程定時執(zhí)行 runtime.sysmon 函數(shù),這個函數(shù)主要用來檢查死鎖、運行計時器、調度搶占、以及 GC 等。

          它會執(zhí)行 runtime.gcTrigger中的 test 函數(shù)來判斷是否應該進行 GC。由于 GC 可能需要執(zhí)行時間比較長,所以運行時會在應用程序啟動時在后臺開啟一個用于強制觸發(fā)垃圾收集的 Goroutine 執(zhí)行 forcegchelper 函數(shù)。

          不過 forcegchelper 函數(shù)在一般情況下會一直被 goparkunlock 函數(shù)一直掛起,直到 sysmon 觸發(fā)GC 校驗通過,才會將該被掛起的 Goroutine 放轉身到全局調度隊列中等待被調度執(zhí)行 GC。

          runtime.GC

          這個比較簡單,會獲取當前的 GC 循環(huán)次數(shù),然后設值為 gcTriggerCycle 模式調用 gcStart 進行循環(huán)。

          runtime.mallocgc

          我在內存分配 https://www.luozhiyun.com/archives/434 這一節(jié)講過,對象在進行內存分配的時候會按大小分成微對象、小對象和大對象三類分別執(zhí)行 tiny malloc、small alloc、large alloc。

          Go 的內存分配采用了池化的技術,類似 CPU 這樣的設計,分為了三級緩存,分別是:每個線程單獨的緩存池mcache、中心緩存 mcentral 、堆頁 mheap 。

          tiny malloc、small alloc 都會先去 mcache 中找空閑內存塊進行內存分配,如果 mcache 中分配不到內存,就要到 mcentral 或 mheap 中去申請內存,這個時候就會嘗試觸發(fā) GC;而對于 large alloc 一定會嘗試觸發(fā) GC 因為它直接在堆頁上分配內存。

          如何控制 GC 是否應該被執(zhí)行?

          上面這三個觸發(fā) GC 的地方最終都會調用 gcStart 執(zhí)行 GC,但是在執(zhí)行 GC 之前一定會先判斷這次調用是否應該被執(zhí)行,并不是每次調用都一定會執(zhí)行 GC, 這個時候就要說一下 runtime.gcTrigger中的 test 函數(shù),這個函數(shù)負責校驗本次 GC 是否應該被執(zhí)行。

          runtime.gcTrigger中的 test 函數(shù)最終會根據(jù)自己的三個策略,判斷是否應該執(zhí)行GC:

          • ? gcTriggerHeap:按堆大小觸發(fā),堆大小和上次 GC 時相比達到一定閾值則觸發(fā);

          • ? gcTriggerTime:按時間觸發(fā),如果超過 forcegcperiod(默認2分鐘) 時間沒有被 GC,那么會執(zhí)行GC;

          • ? gcTriggerCycle:沒有開啟垃圾收集,則觸發(fā)新的循環(huán);

          如果是 gcTriggerHeap 策略,那么會根據(jù) runtime.gcSetTriggerRatio 函數(shù)中計算的值來判斷是否要進行 GC,主要是由環(huán)境變量 GOGC(默認值為100 ) 決定閾值是多少。

          我們可以大致認為,觸發(fā) GC 的時機是由上次 GC 時的堆內存大小,和當前堆內存大小值對比的增長率來決定的,這個增長率就是環(huán)境變量 GOGC,默認是 100 ,計算公式可以大體理解為:

          hard_target = live_dataset + live_dataset * (GOGC / 100).

          假設目前是 100M 內存占用,那么根據(jù)上面公式,會到 200M 的時候才會觸發(fā) GC。

          觸發(fā) GC 的時機其實并不只是 GOGC 單一變量決定的,在代碼 runtime.gcSetTriggerRatio 里面我們可以看到它控制的是一個范圍:

          func gcSetTriggerRatio(triggerRatio float64) { 
              // gcpercent 由環(huán)境變量 GOGC 決定
              if gcpercent >= 0 {
                  // 默認是 1
                  scalingFactor := float64(gcpercent) / 100 
                  // 最大的 maxTriggerRatio 是 0.95
                  maxTriggerRatio := 0.95 * scalingFactor
                  if triggerRatio > maxTriggerRatio {
                      triggerRatio = maxTriggerRatio
                  }

                  // 最大的 minTriggerRatio 是 0.6
                  minTriggerRatio := 0.6 * scalingFactor
                  if triggerRatio < minTriggerRatio {
                      triggerRatio = minTriggerRatio
                  }
              } else if triggerRatio < 0 { 
                  triggerRatio = 0
              }
              memstats.triggerRatio = triggerRatio

              trigger := ^uint64(0)
              if gcpercent >= 0 {
                  // 當前標記存活的大小乘以1+系數(shù)triggerRatio
                  trigger = uint64(float64(memstats.heap_marked) * (1 + triggerRatio))
                  ...
              }
              memstats.gc_trigger = trigger
              ...
          }

          具體閾值計算是比較復雜的,從 gcControllerState.endCycle 函數(shù)中可以看到執(zhí)行 GC 的時機還要看以下幾個因素:

          • ? 當前 CPU 占用率,GC 標記階段最高不能超過整個應用的 25%;

          • ? 輔助 GC 標記對象 CPU 占用率;

          • ? 目標增長率(預估),該值等于:(下次 GC 完后堆大小 - 堆存活大?。? 堆存活大小;

          • ? 堆實際增長率:堆總大小/上次標記完后存活大小-1;

          • ? 上次GC時觸發(fā)的堆增長率大?。?/p>

          這些綜合因素計算之后得到的一個值就是本次的觸發(fā) GC 堆增長率大小。這些都可以通過 GODEBUG=gctrace=1,gcpacertrace=1 打印出來。

          下面我們看看一個具體的例子:

          package main

          import (
              "fmt"
          )

          func allocate() {
              _ = make([]byte1<<20)
          }

          func main() {
              fmt.Println("start.")

              fmt.Println("> loop.")
              for {
                  allocate()
              }
              fmt.Println("< loop.")
          }

          使用 gctrace 跟蹤 GC 情況:

          [root@localhost gotest]# go build main.go 
          [root@localhost gotest]# GODEBUG=gctrace=1 ./main
          start.
          > loop.
          ...
          gc 1409 @0.706s 14%: 0.009+0.22+0.076 ms clock, 0.15+0.060/0.053/0.033+1.2 ms cpu, 4->6->2 MB, 5 MB goal, 16 P
          gc 1410 @0.706s 14%: 0.007+0.26+0.092 ms clock, 0.12+0.050/0.070/0.030+1.4 ms cpu, 4->7->3 MB, 5 MB goal, 16 P
          gc 1411 @0.707s 14%: 0.007+0.36+0.059 ms clock, 0.12+0.047/0.092/0.017+0.94 ms cpu, 5->7->2 MB, 6 MB goal, 16 P
          ...
          < loop.

          上面展示了 3 次 GC 的情況,下面我們看看:

          gc 1410 @0.706s 14%: 0.007+0.26+0.092 ms clock, 0.12+0.050/0.070/0.030+1.4 ms cpu, 4->7->3 MB, 5 MB goal, 16 P

          內存
          4 MB:標記開始前堆占用大小 (in-use before the Marking started)
          7 MB:標記結束后堆占用大小 (in-use after the Marking finished)
          3 MB:標記完成后存活堆的大小 (marked as live after the Marking finished)
          5 MB goal:標記完成后正在使用的堆內存的目標大小 (Collection goal)

          可以看到這里標記結束后堆占用大小是7 MB,但是給出的目標預估值是 5 MB,你可以看到回收器超過了它設定的目標2 MB,所以它這個目標值也是不準確的。

          在 1410 次 GC 中,最后標記完之后堆大小是 3 MB,所以我們可以大致根據(jù) GOGC 推測下次 GC 時堆大小應該不超過 6MB,所以我們可以看看 1411 次GC:

          gc 1411 @0.707s 14%: 0.007+0.36+0.059 ms clock, 0.12+0.047/0.092/0.017+0.94 ms cpu, 5->7->2 MB, 6 MB goal, 16 P

          內存
          5 MB:標記開始前堆占用大小 (in-use before the Marking started)
          7 MB:標記結束后堆占用大小 (in-use after the Marking finished)
          2 MB:標記完成后存活堆的大小 (marked as live after the Marking finished)
          6 MB goal:標記完成后正在使用的堆內存的目標大小 (Collection goal)

          可以看到在 1411 次GC啟動時堆大小是 5 MB 是在控制范圍之內。

          說了這么多 GC 的機制,那么有沒有可能 GC 的速度趕不上制造垃圾的速度呢?這就引出了 GC 中的另一種機制:Mark assist。

          如果收集器確定它需要減慢分配速度,它將招募應用程序 Goroutines 來協(xié)助標記工作。這稱為 Mark assist 標記輔助。這也就是為什么在分配內存的時候還需要判斷要不要執(zhí)行 mallocgc 進行 GC。

          在進行 Mark assist 的時候 Goroutines 會暫停當前的工作,進行輔助標記工作,這會導致當前 Goroutines 工作的任務有一些延遲。

          而我們的 GC 也會盡可能的消除 Mark assist ,所以會讓下次的 GC 時間更早一些,也就會讓 GC 更加頻繁的觸發(fā)。

          我們可以通過 go tool trace 來觀察到 Mark assist 的情況:

          Go Memory Ballast

          上面我們熟悉了 Go GC 的策略之后,我們來看看 Go Memory Ballast 是怎么優(yōu)化 GC 的。下面先看一個例子:

          func allocate() {
              _ = make([]byte1<<20)
          }

          func main() {
              ballast := make([]byte200*1024*1024// 200M 
              for i := 0; i < 10; i++ {
                  go func() {
                      fmt.Println("start.")

                      fmt.Println("> loop.")
                      for {
                          allocate()
                      }
                      fmt.Println("< loop.")
                  }()
              } 
              runtime.KeepAlive(ballast)

          我們運行上面的代碼片段,然后我們對資源利用的情況進行簡單的統(tǒng)計:

          從上面的結果我們可以直到,GC 的 CPU 利用率大約在 5.5 % 左右。

          下面我們把 ballast 內存占用去掉,看看會是多少:

          可以看到在沒有 ballast 的時候 GC 的 CPU占用在 28% 左右。對 GC 的其他信息感興趣的朋友可以使用 runtime.Memstats 定期抓取 GC 的信息進行打印。

          那么為什么在申請了一個空的數(shù)組之后 CPU 占用會低這么多?首先我們在概述也講到了,GC 會根據(jù)環(huán)境變量 GOGC 來決定下次 GC 的執(zhí)行時機,所以如果我們申請了200M的數(shù)組,那么下次 GC 的時候大約會在 400M。由于我們上面的例子中,allocate 函數(shù)申請的對象都是臨時對象,在 GC 之后會被再次減少到 200M 左右,所以下次執(zhí)行 GC 的時機會被再次設置到 400M 。

          但是如果沒有 ballast 數(shù)組,感興趣的可以自行去測試一下,大約會在 4M 左右的時候會觸發(fā) GC,這無疑對于臨時變量比較多的系統(tǒng)來說會造成相當頻繁的 GC。

          總之,通過設置 ballast 數(shù)組我們達到了延遲 GC 的效果,但是這種效果只會在臨時變量比較多的系統(tǒng)中有用,對于全局變量多的系統(tǒng),用處不大。

          那么還有一個問題,在系統(tǒng)中無故申請 200M 這么大的內存會不會對內存造成浪費?畢竟內存這么貴。其實不用擔心,只要我們沒有對 ballast 數(shù)組進行讀寫,是不會真正用到物理內存占用的,我們可以用下面的例子看一下:

          func main() {
              _ = make([]byte, 100<<20)
              <-time.After(time.Duration(math.MaxInt64))
          }

          $ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 4-5 | numfmt --header --from-unit=1024 --to=iec --field 6-7 | column -t | egrep "[t]est|[P]ID"
          %MEM  COMMAND     PID    MAJFL  MINFL  RSS   VSZ
          0.0   test_alloc  31248  0      1.1K   7.4M  821M

          可以看到虛擬內存VSZ占用很大,但是RSS 進程分配的內存大小很小。

          func main() {
              ballast := make([]byte, 100<<20)
              for i := 0; i < len(ballast)/2; i++ {
                  ballast[i] = byte('A')
              }
              <-time.After(time.Duration(math.MaxInt64))
          }

          $ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 4-5 | numfmt --header --from-unit=1024 --to=iec --field 6-7 | column -t | egrep "[t]est|[P]ID"
          %MEM  COMMAND     PID    MAJFL  MINFL  RSS   VSZ
          0.4   test_alloc  31692  0      774    60M   821M

          但是如果我們要對它進行寫入操作,RSS 進程分配的內存大小就會變大,剩下的可以自己去驗證。

          對于 Go Ballast 的討論其實很早就有人提過 issue#23044 ,其實官方只需要加一個最小堆大小的參數(shù)即可,但是一直沒有得到實現(xiàn)。相比之下 Java 就好很多GC 的調優(yōu)參數(shù),InitialHeapSize 就可以設置堆的初始值。

          這也導致了很多對性能要求比較高的項目如: tidb,cortex 都在代碼里加了一個這樣的空數(shù)組實現(xiàn)。

          Go GC Tuner

          這個方法其實是來自 uber 的這篇文章里面介紹的。根本問題還是因為 Go 的 GC 太頻繁了,導致標記占用了很高的 CPU,但是 Go 也提供了 GOGC 來調整 GC 的時機,那么有沒有一種辦法可以動態(tài)的根據(jù)當前的內存調整 GOGC 的值,由此來控制 GC 的頻率呢?

          在 Go 中其實提供了  runtime.SetFinalizer 函數(shù),它會在對象被 GC 的時候最后回調一下。在 Go 中 它是這么定義的:

          type any = interface{}

          func SetFinalizer(obj any, finalizer any)

          obj 一般來說是一個對象的指針;finalizer 是一個函數(shù),它接受單個可以直接用 obj 類型值賦值的參數(shù)。也就是說 SetFinalizer 的作用就是將 obj 對象的析構函數(shù)設置為 finalizer,當垃圾收集器發(fā)現(xiàn) obj 不能再直接或間接訪問時,它會清理 obj 并調用 finalizer。

          所以我們可以通過它來設置一個鉤子,每次 GC 完之后檢查一下內存情況,然后設置 GOGC 值:

          type finalizer struct {
              ref *finalizerRef
          }

          type finalizerRef struct {
              parent *finalizer
          }

          func finalizerHandler(f *finalizerRef) {
              // 為 GOGC 動態(tài)設值
              getCurrentPercentAndChangeGOGC()
            // 重新設置回去,否則會被真的清理
              runtime.SetFinalizer(f, finalizerHandler)
          }

          func NewTuner(options ...OptFunc) *finalizer {
            // 處理傳入的參數(shù)
            ...
            
            f := &finalizer{}
              f.ref = &finalizerRef{parent: f}
              runtime.SetFinalizer(f.ref, finalizerHandler)
            // 設置為 nil,讓 GC 認為原 f.ref 函數(shù)是垃圾,以便觸發(fā) finalizerHandler 調用
              f.ref = nil
            return f
          }

          上面的這段代碼就利用了 finalizer 特性,在 GC 的時候會調用 getCurrentPercentAndChangeGOGC 重新設置 GOGC 值,由于 finalizer 會延長一次對象的生命周期,所以我們可以在 finalizerHandler 中設置完 GOGC 之后再次調用 SetFinalizer 將對象重新綁定在 Finalizer 上。

          這樣構成一個循環(huán),每次 GC 都會有一個 finalizerRef 對象在動態(tài)的根據(jù)當前內存情況改變 GOGC 值,從而達到調整 GC 次數(shù),節(jié)約資源的目的。

          上面我們也提到過,GC 基本上根據(jù)本次 GC 之后的堆大小來計算下次 GC 的時機:

          hard_target = live_dataset + live_dataset * (GOGC / 100).

          比如本次 GC 完之后堆大小 live_dataset 是 100 M,對于 GOGC 默認值 100 來說會在堆大小 200M 的時候觸發(fā) GC。

          為了達到最大化利用內存,減少 GC 次數(shù)的目的,那么我們可以將 GOGC 設置為:

          (可使用內存最大百分比 - 當前占內存百分比)/當前占內存百分比 * 100

          也就是說如果有一臺機器,全部內存都給我們應用使用,應用當前占用 10%,也就是 100M,那么:

           GOGC = (100%-10%)/10% * 100 = 900

          然后根據(jù)上面 hard_target 計算公式可以得知,應用將在堆占用達到 1G 的時候開始 GC。當然我們生產當中不可能那么極限,具體的最大可使用內存最大百分比還需要根據(jù)當前情況進行調整。

          那么換算成代碼,我們的 getCurrentPercentAndChangeGOGC 就可以這么寫:

          var memoryLimitInPercent float64 = 100

          func getCurrentPercentAndChangeGOGC() {
              p, _ := process.NewProcess(int32(os.Getpid()))
            // 獲取當前應用占用百分比
            memPercent, _ := p.MemoryPercent()
            // 計算 GOGC 值
            newgogc := (memoryLimitInPercent - float64(memPercent)) / memPercent * 100.0
            // 設置 GOGC 值
            debug.SetGCPercent(int(newgogc))
          }

          上面這段代碼我省去了很多異常處理,默認處理,以及 memoryLimitInPercent 寫成了一個固定值,在真正使用的時候,代碼還需要再完善一下。

          寫到這里,上面 Go Memory Ballast 和 Go GC Tuner 已經達到了我們的優(yōu)化目的,但是在我即將提稿的時候,曹春暉大佬發(fā)了一篇文章中,說到最新的 Go 版本中 1.19 beta1版本中新加了一個 debug.SetMemoryLimit 函數(shù)。

          Soft Memory Limit

          這一個優(yōu)化來自 issue#48409,在 Go 1.19 版本中被加入,優(yōu)化原理實際上和上面差不多,通過內置的 debug.SetMemoryLimit 函數(shù)我們可以調整觸發(fā) GC 的堆內存目標值,從而減少 GC 次數(shù),降低GC 時 CPU 占用的目的。

          在上面我們也講了,Go 實現(xiàn)了三種策略觸發(fā) GC ,其中一種是 gcTriggerHeap,它會根據(jù)堆的大小設定下次執(zhí)行 GC 的堆目標值。1.19 版的代碼正是對 gcTriggerHeap 策略做了修改。

          通過代碼調用我們可以知道在 gcControllerState。heapGoalInternal 計算 HeapGoal 的時候使用了兩種方式,一種是通過 GOGC 值計算,另一種是通過 memoryLimit 值計算,然后取它們兩個中小的值作為 HeapGoal。

          func (c *gcControllerState) heapGoalInternal() (goal, minTrigger uint64) {
              // Start with the goal calculated for gcPercent.
              goal = c.gcPercentHeapGoal.Load() //通過 GOGC 計算 heapGoal

              // 通過 memoryLimit 計算 heapGoal,并和 goal 比較大小,取小的
              if newGoal := c.memoryLimitHeapGoal(); go119MemoryLimitSupport && newGoal < goal {
                  goal = newGoal
              } else {
                  ...
              }
              return
          }

          gcPercentHeapGoal 的計算方式如下:

          func (c *gcControllerState) commit(isSweepDone bool) {
              ...
              gcPercentHeapGoal := ^uint64(0)
              if gcPercent := c.gcPercent.Load(); gcPercent >= 0 {
                  // HeapGoal = 存活堆大小 + (存活堆大小+棧大小+全局變量大?。? GOGC/100
                  gcPercentHeapGoal = c.heapMarked + (c.heapMarked+atomic.Load64(&c.lastStackScan)+atomic.Load64(&c.globalsScan))*uint64(gcPercent)/100
              }
              c.gcPercentHeapGoal.Store(gcPercentHeapGoal)
              ...
          }

          和我們上面提到的 hard_target 計算差別不大,可以理解為:

          HeapGoal = live_dataset + (live_dataset+棧大小+全局變量大小)* GOGC/100

          我們再看看memoryLimitHeapGoal計算:

          func (c *gcControllerState) memoryLimitHeapGoal() uint64 { 
              var heapFree, heapAlloc, mappedReady uint64
              heapFree = c.heapFree.load()                          
              heapAlloc = c.totalAlloc.Load() - c.totalFree.Load()  
              mappedReady = c.mappedReady.Load()                   
              
              memoryLimit := uint64(c.memoryLimit.Load())
           
              nonHeapMemory := mappedReady - heapFree - heapAlloc 
              ...
              goal := memoryLimit - nonHeapMemory

              ...
              return goal
          }

          上面這段代碼基本上可以理解為:

          goal = memoryLimit - 非堆內存

          所以正因為 Go GC 的觸發(fā)是取上面兩者計算結果較小的值,那么原本我們使用 GOGC 填的太大怕導致 OOM,現(xiàn)在我們可以加上 memoryLimit 參數(shù)限制一下;或者直接 GOGC = off ,然后設置 memoryLimit 參數(shù),通過它來調配我們的 GC。

          總結

          我們這篇主要通過講解 Go GC 的觸發(fā)機制,然后引出利用這個機制可以比較 hack 的方式減少 GC 次數(shù),從而達到減少 GC 消耗。

          Go Memory Ballast 主要是通過預設一個大數(shù)組,讓 Go 在啟動的時候提升 Go 下次觸發(fā) GC 的堆內存閾值,從而避免在內存夠用,但是應用內臨時變量較多時不斷 GC 所產生的不必要的消耗。

          Go GC Tuner 主要是通過 Go 提供的 GC 鉤子,設置 Finalizer 在 GC 完之后通過當前的內存使用情況動態(tài)設置 GOGC,從而達到減少 GC 的目的。

          Soft Memory Limit 是1.19版本的新特性,通過內置的方式實現(xiàn)了 GC 的控制,通過設置 memoryLimit 控制 GC 內存觸發(fā)閾值達到減少 GC 的目的,原理其實和上面兩種方式沒有本質區(qū)別,但是由于內置在 GC 環(huán)節(jié),可以更精細化的檢查當前的非堆內存占用情況,從而實現(xiàn)更精準控制。

          Reference

          https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/

          https://github.com/golang/go/issues/23044

          https://www.cnblogs.com/457220157-FTD/p/15567442.html

          https://github.com/golang/go/issues/42430

          https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/

          https://xargin.com/dynamic-gogc/

          https://github.com/cch123/gogctuner

          https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/pacing/

          https://medium.com/a-journey-with-go/go-finalizers-786df8e17687

          https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector

          https://xargin.com/the-new-api-for-heap-limit/

          https://pkg.go.dev/runtime/debug@master#SetMemoryLimit

          https://tip.golang.org/doc/go1.19

          https://github.com/golang/go/issues/48409



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲??;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 93
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线播放A片 | 日BB视频| 影音先锋成人资源在线观看 | 色色无码 | 国产精品嫩草AV城中村 |