<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』Go 內(nèi)存管理概述

          共 4920字,需瀏覽 10分鐘

           ·

          2021-06-08 07:19

          原文地址:https://medium.com/safetycultureengineering/an-overview-of-memory-management-in-go-9a72ec7c76a8

          原文作者:Scott Gangemi

          本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w21_An_overview_of_memory_management_in_Go.md

          譯者:haoheipi

          校對(duì):cvley、fivezh

          隨著程序的運(yùn)行,對(duì)象被寫入內(nèi)存。在一些特定時(shí)刻當(dāng)它們不再被需要時(shí),它們應(yīng)該被移除。這個(gè)過程被稱為 內(nèi)存管理 。本文旨在給出內(nèi)存管理的概述,然后深入研究在 Go 中如何使用垃圾收集器實(shí)現(xiàn)內(nèi)存管理。Go 的內(nèi)存管理近些年已經(jīng)發(fā)生了很大變化,未來很可能還會(huì)發(fā)生更多變化。如果您正在閱讀這篇文章,并且您使用的是比 1.16 更高的 Go 版本,那么這里的一些信息可能已經(jīng)過時(shí)了。

          手動(dòng)內(nèi)存管理

          在像 C 這樣的編程語言中,程序員會(huì)調(diào)用 malloccalloc 之類的函數(shù)來將對(duì)象寫入內(nèi)存。這些函數(shù)返回一個(gè)指針,指向該對(duì)象在堆內(nèi)存中的位置。當(dāng)這個(gè)對(duì)象不再被需要時(shí),程序員調(diào)用 free 函數(shù)來再釋放以便再次使用這塊內(nèi)存。這種內(nèi)存管理的方法被稱為 顯式釋放 。它非常的強(qiáng)大,使程序員能夠更好地控制正在使用的內(nèi)存,從而允許某些類型優(yōu)化變得更加容易,特別是在小內(nèi)存環(huán)境下。但是,它也會(huì)導(dǎo)致兩種類型的編程錯(cuò)誤。

          第一種是提前調(diào)用 free ,這會(huì)創(chuàng)建一個(gè) 懸空指針 。懸空指針是指不再指向內(nèi)存中有效對(duì)象的指針。那么這會(huì)非常糟糕,因?yàn)槌绦蚱谕粋€(gè)指針指向的是已定義的值,而當(dāng)這個(gè)懸空指針稍后被訪問時(shí),并不能保證在內(nèi)存中該位置存在什么值。可能什么都沒有,或者完全是其他值。第二種錯(cuò)誤,內(nèi)存根本無法釋放。如果程序員忘記釋放一個(gè)對(duì)象,他們可能會(huì)面臨 內(nèi)存泄漏 風(fēng)險(xiǎn),因?yàn)閮?nèi)存會(huì)被越來越多的對(duì)象填滿。如果內(nèi)存不足,這可能導(dǎo)致程序變慢或崩潰。所以當(dāng)不得不顯式地管理內(nèi)存時(shí),可能會(huì)在程序中引入不可預(yù)測(cè)的錯(cuò)誤。

          自動(dòng)內(nèi)存管理

          這是像 Go 這樣的語言提供了 自動(dòng)的動(dòng)態(tài)內(nèi)存管理 ,或者更簡(jiǎn)單地說,垃圾收集 的原因。具有垃圾收集功能的語言提供了如下好處:

          • 安全性的提高
          • 更好的跨操作系統(tǒng)移植性
          • 需要編寫的代碼更少
          • 代碼的運(yùn)行時(shí)校驗(yàn)
          • 數(shù)組的邊界檢查

          確實(shí)垃圾收集會(huì)帶來性能開銷,但并不像通常認(rèn)為的那樣多。所以折衷的方案是,程序員專注于他們程序的業(yè)務(wù)邏輯,并確保它符合目標(biāo),而不用擔(dān)心管理內(nèi)存。

          一個(gè)正在運(yùn)行的程序?qū)?duì)象存儲(chǔ)在內(nèi)存中的兩個(gè)位置, 堆 和 棧 。垃圾收集作用于堆上,而不是棧。棧是一個(gè)存儲(chǔ)函數(shù)值的后進(jìn)先出數(shù)據(jù)結(jié)構(gòu)。從函數(shù)內(nèi)部調(diào)用另一個(gè)函數(shù),會(huì)將一個(gè)新的 棧幀 放到棧上,它包含被調(diào)用函數(shù)的值等。當(dāng)函數(shù)調(diào)用返回時(shí),它的棧楨將會(huì)從棧上彈出。當(dāng)在調(diào)試一個(gè)崩潰的程序時(shí),您可能會(huì)熟悉棧這一結(jié)構(gòu)。大多數(shù)語言的編譯器會(huì)返回一個(gè)調(diào)用棧來幫助跟蹤調(diào)試,它會(huì)顯示在這一點(diǎn)之前被調(diào)用的函數(shù)。


          棧可以以一種后進(jìn)先出的方式將值 “推” 到頂部,或者從頂部 “彈出” 。圖片來源 Wikipedia.

          與棧相反,堆中包含的是在函數(shù)外部被引用的值。例如,在程序開始時(shí)定義的靜態(tài)常量,或更復(fù)雜的對(duì)象,如 Go 結(jié)構(gòu)體。當(dāng)程序員定義一個(gè)放置在堆上的對(duì)象時(shí),將分配所需的內(nèi)存大小,并返回指向該對(duì)象的指針。堆是一種圖結(jié)構(gòu),對(duì)象代表著節(jié)點(diǎn),這些節(jié)點(diǎn)被代碼或者其他對(duì)象所引用。隨著程序的運(yùn)行,堆將隨著對(duì)象的添加而繼續(xù)增長(zhǎng),除非對(duì)堆做清理。

          堆從根節(jié)點(diǎn)開始,隨著更多的對(duì)象被添加而增長(zhǎng)。

          Go 中的垃圾收集

          Go 更喜歡在棧上分配內(nèi)存,所以大部分內(nèi)存分配都會(huì)在這里結(jié)束。這也意味著 Go 中每個(gè) goroutine 都有一個(gè)棧,如果可能的話,Go 將分配變量在這個(gè)棧上。Go 編譯器通過執(zhí)行 逃逸分析 來檢查一個(gè)對(duì)象是否 ”逃逸” 出函數(shù)內(nèi)部,從而嘗試證明一個(gè)變量在函數(shù)之外不被需要。如果編譯器可以確定一個(gè)變量的 生命周期,它將被分配在棧上。但是,如果變量的生存期不確定,它將會(huì)被分配到堆上。通常,如果一個(gè) Go 程序有一個(gè)指向?qū)ο蟮闹羔槪敲丛搶?duì)象就被存儲(chǔ)在堆上。看看下面的示例代碼:

          type myStruct struct {
            value int
          }
          var testStruct = myStruct{value: 0}
          func addTwoNumbers(a int, b int) int {
            return a + b
          }
          func myFunction() {
            testVar1 := 123
            testVar2 := 456
            testStruct.value = addTwoNumbers(testVar1, testVar2)
          }
          func someOtherFunction() {
            // some other code
            myFunction()
            // some more code
          }

          出于本例的目的,讓我們假設(shè)這是一個(gè)正在運(yùn)行的程序的一部分,因?yàn)槿绻@是整個(gè)程序,那么 Go 編譯器會(huì)通過將變量分配到棧來優(yōu)化它。當(dāng)程序運(yùn)行時(shí):

          1. testStruct 是被定義和放置在堆中的一個(gè)可用內(nèi)存塊
          2. myFunction 函數(shù)被調(diào)用執(zhí)行時(shí)將會(huì)分配一個(gè)棧。testVar1testVar2 都被存儲(chǔ)在這個(gè)棧上。
          3. 調(diào)用 addTwoNumbers 時(shí),一個(gè)新的棧幀被推到棧上,并帶有函數(shù)的兩個(gè)參數(shù)。
          4. 當(dāng) addTwoNumbers 完成執(zhí)行,它的結(jié)果返回給 myFunction 并且 addTwoNumbers 的棧幀從棧中彈出,因?yàn)樗辉俦恍枰?/section>
          5. 指向 testStruct 的指針被跟隨到它堆上的位置,并且 value 字段被更新。
          6. myFunction 退出,并清除為它創(chuàng)建的棧。testStruct 的值繼續(xù)保持在堆上,直到垃圾收集發(fā)生。

          testStruct 現(xiàn)在在堆上,也沒有使用,Go 運(yùn)行時(shí)也不知道是否仍然需要它。為此,Go 依賴于一個(gè)垃圾收集器。垃圾收集器有兩個(gè)關(guān)鍵部分,一個(gè) 更改器 和一個(gè) 收集器。收集器執(zhí)行垃圾收集邏輯并找到應(yīng)該釋放其內(nèi)存的對(duì)象。更改器執(zhí)行應(yīng)用程序代碼并將新對(duì)象分配給堆。它還在程序運(yùn)行時(shí)更新堆上的現(xiàn)有對(duì)象,包括使不再需要的某些對(duì)象變?yōu)椴豢蛇_(dá)。

          由于更改器所做的更改,底部的對(duì)象已變?yōu)椴豢稍L問。它應(yīng)該由垃圾收集器清理。

          Go 垃圾收集器的實(shí)現(xiàn)

          Go 的垃圾收集器是一個(gè) 非分代并發(fā)三色標(biāo)記清除的垃圾收集器。讓我們把這幾項(xiàng)分解。

          分代假設(shè) 是壽命短的對(duì)象(如臨時(shí)變量)最常被回收。因此,分代垃圾收集器主要關(guān)注最近分配的對(duì)象。然而如前所述,編譯器優(yōu)化允許 Go 編譯器將具有已知生命周期的對(duì)象分配在棧上。這意味著堆上的對(duì)象更少,因此垃圾收集的對(duì)象更少。這也意味著在 Go 中不需要分代垃圾收集器。因此,Go 使用了一個(gè)非分代的垃圾收集器。并發(fā)意味著收集器與更改器線程同時(shí)運(yùn)行。因此,Go 使用的是一個(gè)非分代、并發(fā)的垃圾收集器。標(biāo)記清除是垃圾收集器的工作類型,三色是用于實(shí)現(xiàn)這一功能的算法。

          一個(gè)標(biāo)記清除垃圾收集器有兩個(gè)階段,不出所料地命名為 標(biāo)記清除 。在標(biāo)記階段,收集器遍歷堆并標(biāo)記不再需要的對(duì)象。后續(xù)掃描階段將刪除這些對(duì)象。標(biāo)記和清除是一種間接算法,因?yàn)樗鼧?biāo)記活動(dòng)對(duì)象,并移除其他所有東西。

          原圖地址:https://github.com/gocn/translator/raw/master/static/images/w21_An_overview_of_memory_management_in_Go/figure4.gif

          可視化的標(biāo)記清除收集器過程,來源于這里。如果你感興趣的話,還可以看到其他類型的垃圾收集器。

          Go 用幾個(gè)步驟實(shí)現(xiàn)了這一點(diǎn):

          Go 讓所有的 goroutines 到達(dá)一個(gè)垃圾收集安全點(diǎn),并使用一個(gè)名為 stop the world 的過程。這將暫時(shí)停止程序的運(yùn)行,并打開一個(gè) 寫屏障 以維護(hù)堆上的數(shù)據(jù)完整性。通過允許 goroutine 和收集器同時(shí)運(yùn)行,從而實(shí)現(xiàn)了并發(fā)性。

          一旦所有的 goroutine 都打開了寫障礙,Go 運(yùn)行 starts the world 并讓工作線程開始執(zhí)行垃圾收集工作。

          標(biāo)記是通過使用一個(gè) 三色算法 實(shí)現(xiàn)的。當(dāng)標(biāo)記開始時(shí),除了根對(duì)象是灰色的,所有對(duì)象都是白色的。根是所有其他堆對(duì)象的來源,并作為運(yùn)行程序的一部分實(shí)例化。垃圾收集器首先掃描棧、全局變量和堆指針,以了解什么對(duì)象正在使用。當(dāng)掃描一個(gè)棧時(shí),工作線程將停止 goroutine ,并通過從根向下遍歷將所有發(fā)現(xiàn)的對(duì)象標(biāo)記為灰色。然后繼續(xù)執(zhí)行 goroutine 。

          然后,灰色的對(duì)象將入隊(duì)變成黑色,這表明它們?nèi)栽谑褂弥小R坏┧械幕疑珜?duì)象被標(biāo)為黑色,收集器將會(huì)再一次 stop the world 并且清理所有不再被需要的白色節(jié)點(diǎn)對(duì)象。程序現(xiàn)在可以繼續(xù)運(yùn)行,直到它需要再次清理更多內(nèi)存。

          這張來自維基百科的圖表讓上述更容易理解。顏色有點(diǎn)混亂,但白色物體是淺灰色,灰色物體是黃色,黑色物體是藍(lán)色。

          一旦程序按照使用的內(nèi)存比例分配了額外的內(nèi)存,這個(gè)進(jìn)程將再次啟動(dòng)。GOGC 環(huán)境變量決定了這一比例,默認(rèn)值為 100 。Go 的源代碼描述如下:

          如果 GOGC=100 并且我們正在使用 4M 內(nèi)存,我們將在到達(dá) 8M 時(shí)再次進(jìn)行 GC(這個(gè)標(biāo)記在 next_gc 變量中被跟蹤)。這使 GC 成本與分配成本成線性比例。調(diào)整 GOGC 只是改變線性常數(shù)(還有額外內(nèi)存的使用量)。

          Go 的垃圾收集器通過將內(nèi)存管理抽象到 Go 運(yùn)行時(shí)來提高效率,這也是使 Go 具有如此優(yōu)秀性能的原因之一。Go 內(nèi)置的工具允許您優(yōu)化程序中垃圾收集的觸發(fā)行為,如果您感興趣,可以對(duì)此進(jìn)行研究。至此,我希望您了解到了更多關(guān)于垃圾收集的工作原理和在 Go 中如何實(shí)現(xiàn)垃圾收集的知識(shí)。

          參考

          • Garbage Collection in Go: Part 1

          • Getting to Go: The Journey of Go’s Garbage Collector

          • Go: How Does the Garbage Collector Mark the Memory?

          • Golang: Cost of using the heap

          • Golang FAQ

          • Google Groups discussion, comment by Ian Lance Taylor

          • Implementing memory management with Golang’s garbage collector

          • Memory Management Reference

          • Stack (abstract data type)

          • The Garbage Collection Handbook

          • Tracing garbage collection: Tri-color marking

          別忘了還有 Gopher China 2021 大會(huì)在文末等著你哦~


          想和各位技術(shù)大佬們同臺(tái)見面嘛?


          那就趕快點(diǎn)擊下方「閱讀原文」報(bào)名參加呀!

          瀏覽 72
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  乐播一区二区三区 | 国产乱伦小说网站 | 911偷拍网在线偷拍 | av天堂8| 中文字幕永久在线 |