<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垃圾回收系列之五:棧與棧對象

          共 2419字,需瀏覽 5分鐘

           ·

          2021-03-18 22:20

          棧掃描

              在進行根對象掃描的過程中,棧掃描是其中最重要的部分。因為在一個程序中,可能會有成千上萬的協(xié)程棧。棧掃描需要編譯時與運行時的共同努力,運行時能夠計算出當前協(xié)程棧的所有棧幀信息,而編譯時能夠得知棧上哪些地方有指針,以及對象中的哪一個部分包含了指針。

              每一個函數(shù)在執(zhí)行過程中都使用一塊棧內(nèi)存用來保存返回地址、局部變量、函數(shù)參數(shù)等,我們將這一塊區(qū)域稱為某函數(shù)的棧幀(stack frame)。

              當發(fā)生函數(shù)調(diào)用時,因為調(diào)用者還沒有執(zhí)行完,其棧內(nèi)存中保存的數(shù)據(jù)還有用,所以被調(diào)用函數(shù)不能覆蓋調(diào)用者的棧幀,只能把被調(diào)用函數(shù)的棧幀壓棧,等被調(diào)函數(shù)執(zhí)行完成后再把其棧幀出棧。這樣,棧的大小就會隨函數(shù)調(diào)用層級的增加而生長,隨函數(shù)的返回而縮小,也就是說函數(shù)調(diào)用層級越深,消耗的棧空間就越大。

               因為數(shù)據(jù)是以先進先出的方式添加和刪除的,所以基于堆棧的內(nèi)存分配非常簡單,并且通常比基于堆的動態(tài)內(nèi)存分配內(nèi)存快得多。另外,當函數(shù)退出時,堆棧上的內(nèi)存會自動高效地回收,這是垃圾回收一種最初的形式。雖然維護和管理函數(shù)的棧幀非常重要,但是通常對于高級編程語言來說是隱藏的。例如Go語言中借助于編譯器,在開發(fā)中不用關心局部變量在棧中的布局與釋放。許多計算機指令集在硬件級別提供了用于管理棧的特殊指令,例如80x86指令集提供的SP寄存器用于管理棧,

          以A函數(shù)調(diào)用B函數(shù)為例,抽象的函數(shù)棧的結(jié)構如下所示。

          Go語言中實際的棧幀布局如下所示(源代碼中的注釋):

              運行時可以計算出當前棧幀的函數(shù)參數(shù)、函數(shù)本地變量、寄存器信息SP、BP等一系列信息。

              對每一個棧幀函數(shù)中的參數(shù)和局部變量,都需要對其進行掃描,掃描該對象是否仍然在使用。如果在使用,需要掃描bytedata位圖判斷對象中是否包含指針,如果包含指針則需要進行標記。其中函數(shù)執(zhí)行到某一位置時,與某個參數(shù)和局部變量對應的位圖bytedata是借助于編譯時計算出來的。

          func scanframeworker(frame *stkframe, state *stackScanState, gcw *gcWork) {
          // 掃描局部變量
          if locals.n > 0 {
          size := uintptr(locals.n) * sys.PtrSize
          scanblock(frame.varp-size, size, locals.bytedata, gcw, state)
          }

          // 掃描函數(shù)參數(shù)
          if args.n > 0 {
          scanblock(frame.argp, uintptr(args.n)*sys.PtrSize, args.bytedata, gcw, state)
          }
          }

              什么情況下對象可能沒有在使用了呢?例如如下所示,當foo()函數(shù)執(zhí)行到調(diào)用bar() 函數(shù)時,局部對象t就已經(jīng)沒有被使用了,所以即便對象t中有指針,位圖bytedata中全為0,代表參數(shù)不再被使用。一個不再被使用的對象,可以被回收,不需要再進行掃描。

          func  foo(){
          t := T{}
          t.a = 2
          bar()
          }

          棧對象(stack object)

              在Go語言早期就是通過上述方式對協(xié)程棧中的對象進行掃描的。但是這種方法在有些情況下會出現(xiàn)問題,例如在如下函數(shù)中, 對象t首先被p所引用,但是在之后的程序中,變量p的值發(fā)生了變化,這意味著,t其實并沒有使用了。但是編譯器由于難以知道P在何時會重新賦值導致t不再被引用,因此,編譯器會采取保守的策略認為t對象仍然存在,從而,如果對象t中有指針指向了堆內(nèi)存,就造成了內(nèi)存泄露問題。因為這部分內(nèi)存本應該被釋放。

          t := T{...}
          p := &t
          for {
          if … {
          p = …
          }
          }

              為了解決內(nèi)存泄露的問題,Go語言引進了 棧對象(stack object) 的概念。棧對象是在棧上能夠被尋址的對象。例如上例中的t,由于其能夠被&t的形式尋址,其一定在棧上有地址。所以t就被叫做 棧對象。因為并不是所有的變量都會存儲在棧上,例如存儲在寄存器中的變量就是不能被尋址的。

              首先,編譯器會在編譯時將所有的棧對象記錄下來,在垃圾回收期間,所有的棧對象會存儲到一顆二叉搜索樹中。接著,第二步將棧中所有可能指向棧對象的指針都進行追蹤。

              如下所示,假設F為一個局部變量指針,其引用了棧幀上的棧對象E→C→D→A, 因此說明棧對象E、C、D、A都是存活的,需要被掃描。 相反如果棧對象B沒有被掃描,并且接下來在foo()函數(shù)中沒有使用到B對象,那么B棧對象不會被掃描,從而解決了內(nèi)存泄露問題。




          推薦閱讀


          福利

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

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  五月天堂婷婷 | 亚洲精品成人片在线播放波多野吉 | 强开小嫩苞一区二区电影 | 手机免费在线看片网址av | 亚洲AV永久无码国产精品国产 |