Lua5.4 分代gc 的理解


1.為什么使用的是 GC 而不是 RC?
  可以這樣說,GC (garbage collection) 把 RC (reference counting) 中那些短期對象的銷毀代價轉嫁到了一次性的標記清除過程。這把邏輯處理和資源管理正交分解了。這種被分解的問題,會隨着硬件的進步更容易提高性能(比如多核的發展)。但是,在較小規模的軟件或獨立模塊中,這個優勢並不會太明顯。反而 GC 本身遠高於 RC 的復雜性,會成為其軟肋。
  參考雲風的博客: https://blog.codingnow.com/2008/06/gc.html
2.為什么要使用分代gc?
  根據局部性原理(計算機能發展到現在階段,最主要的就是依賴局部性原理,例如虛擬內存),一個對象在某個時間點被訪問了,那么接下來的一段時間內會重復的訪問該對象。
  那么如果使用Lua5.2中的四色標記法,每次都會將此輪gc中未被使用的對象回收掉,但是這些對象可能是在上一輪存活過的,那么極可能在接下來的一段時間還會多次訪問到(即使這次沒訪問),那么就不應該那么快的回收掉,從而下次還需要申請內存來創建。
3.Lua5.2已經有第一版本的分代gc了,它Lua5.4的分代有何不同?
  最主要的區別在於,Lua5.2版本的gc的老年代對象的判定是存活過一輪即可成為老年代,從而導致大量的臨時變量都成為了老年代而不會在下一次局部gc的時候回收掉,這樣的話內存增長跟分步gc沒什么區別,所以會比較頻繁得切換回分步gc進行全量的gc;而在Lua5.4中,只有當一個對象成功得活過兩輪gc才會被標記未老年代,這樣有效得避免了臨時變量的干擾。
4.分代GC主要的目的是實現什么作用?
  最主要的功能是,避免每次都進行全量掃描,只對新生代的對象進行掃描,這樣可以降低掃描成本從而減少GC成本。
5.開啟分代GC模式:
  在初始化完lvm時,顯示調用 lua_gc(L, LUA_GCGEN, 0, 0); 即可。
6.分代GC的整體架構:
  分代GC主要分兩個GC過程,一個是完整的GC(即major GC),其二是局部GC(即minor GC)。
  當調用 lua_gc(L, LUA_GCGEN, 0, 0); 時會執行luaC_changemode函數進而進入到分代模式中,將會先進行分步模式下的gc掃描以及atomic掃描,之后會將所有存活下來的所有對象都設置為 G_OLD 狀態(這個gc步驟即為 major GC,在進行major GC 前需要先進入到分步GC模式下進行初始化並且執行掃描)。
  當major GC 完成並判斷后進入到分代GC模式,當觸發下一輪GC時,會先判斷上一輪完整GC到當前時間所增加的內存大小,從而判斷是否進入到局部GC(youngcollection,具體數值將在文中后續給出),youngcollection會將此前的對象鏈表中的狀態進行更新以及將新生的對象並且不能達的回收掉。
7.在分代GC中,是怎么觸發下一輪GC的?
  不只是在分代GC中,在分步GC中,也是使用一個字段來判斷是否需要執行新一輪的GC,在global_State 中的 GCdebt 字段,如果在申請內存時發現此字段的值大於等於0時則會進行一次GC。
  特別的,在 global_State 中的 totalbytes 字段,在分代GC中用來選擇執行 major 還是 minor GC。
  具體的使用將在下文提到。
8.在分代GC的執行過程中,major GC 與 minor GC 是怎么配合的?
  接下來討論 totalbytes 字段,文中的所有變量都使用lua默認的值。
  第一步:具體的配合操作在 genstep 中,首先會先判定上一輪的young gc是否為 bad collection。
  第二步:如果不是 bad collection,則會判斷上一輪設置的 totalbytes 字段是否大於 majorbase + majorinc (這里的majorbase 是指上一輪完整GC 所存活下來的對象總數量,majorinc 默認的值是majorbase, majorbase + majorinc 則表示內存增長大於上一輪完整GC的對象數量,如果是則需要進行一次 fullgen(major GC),如果此次 fullgen 回收掉了一半的增長量(即回收了majorinc / 2 對象數量),則下一輪GC繼續進行分代GC(此時會將 GCdebt 設置為20%的值,表示下一次觸發時機為內存增量達到20%時),如果此次 fullgen 沒有回收掉一半增長量則表示當前的是 bad collection(此時會將 GCdebt 設置為100%);如果 totalbytes 字段小於 majorbase + majorinc 則進行一次 young gc(同 GCdebt 設置為20%)。
  如果是一次 bad collection, 則會進行一次 stepgenfull 操作,此操作會重復執行 major GC 操作,只在當前的內存增量小於 1/8 totalbytes 時,下一次觸發gc才會進入第一步中。

  

static void genstep (lua_State *L, global_State *g) {
  if (g->lastatomic != 0)  /* last collection was a bad one? */
    stepgenfull(L, g);  /* do a full step */
  else {
    lu_mem majorbase = g->GCestimate;  /* memory after last major collection */

    // #define getgcparam(p)  ((p) * 4)
    // #define setgcparam(p,v) ((p) = (v) / 4)
    // #define LUAI_GENMAJORMUL         100
    // #define LUAI_GENMINORMUL         20
    lu_mem majorinc = (majorbase / 100) * getgcparam(g->genmajormul);   // (majorbase / 100) * LUAI_GENMAJORMUL = majorbase 
    if (g->GCdebt > 0 && gettotalbytes(g) > majorbase + majorinc) {     // 上一次完整gc到現在期間,內存增長翻倍以上
      lu_mem numobjs = fullgen(L, g);  /* do a major collection */      // 會切換分代與分步模式,為了避免多次進入這里導致切換頻繁,所以在下面加了"bad collection"標記。
      if (gettotalbytes(g) < majorbase + (majorinc / 2)) {              // 重新掃描一次分步模式下的對象數量,如果這次掃描后的新增的對象數量少於掃描前的數量的一半則繼續分代的局部gc
        /* collected at least half of memory growth since last major
           collection; keep doing minor collections */
        setminordebt(g);                                                // 設置下一次minor gc的時機,默認為當前總對象數量的20%新增時觸發
      }
      else {  /* bad collection */
        g->lastatomic = numobjs;  /* signal that last collection was bad */
        setpause(g);  /* do a long wait for next (major) collection */      // 默認情況下是新增量達到上一次存活下來的對象數量的一倍時
      }
    }
    else {  /* regular case; do a minor collection */
      youngcollection(L, g);
      setminordebt(g);                                                  // 設置下一次minor gc的時機,默認為當前總對象數量的20%新增時觸發
      g->GCestimate = majorbase;  /* preserve base value */
    }
  }
  lua_assert(isdecGCmodegen(g));
}

  


9.分代GC中涉及到的狀態變化,各狀態的理解
  old的定義是狀態大於 G_SURVIVAL

  G_NEW:本次cycle創建的新對象(沒有引用任何old對象)
  G_SURVIVAL:上一輪cycle創建的對象 -- 只活過一輪,下一次如果是白色的話,仍然會被回收。
  G_OLD0:表示本次cycle創建的新對象,但是引用了old對象,需要barrier操作。為什么不直接設置成old對象?同G_SURVIVAL,是實現的問題。
  G_NEW,G_SURVIVAL,G_OLD0表示都不是老對象

  G_OLD1:表示作為老對象第一次存活了整個gc過程 -- 為什么需要G_OLD1(從G_SURVIVAL或者G_OLD0變成G_OLD1,而不是直接從G_SURVIVAL或者G_OLD0變成G_OLD)?因為如果節點A現在是在G_SURVIVAL或者G_OLD0,在同一個cycle中轉成old對象前,有一個子節點B引用了A節點(例:A[B]=C),這時候因為A節點仍然不是old,所以不會觸發到barrier與barrier_back操作,所以節點B仍然是G_NEW,到最后,A節點直接轉變成G_OLD的話,B節點轉變成G_SURVIVAL,因為在youngcollection中不會對g->reallyold鏈表進行markold操作,所以在下一次gc中,如果B節點不可達(即是白色)的話,那么在這次gc中會被釋放。雖然可以改成old也傳播,但是這樣的話就破壞了規則(會對所有的G_OLD都遍歷,這樣就達不到縮減成本的目的了)。

  G_OLD:表示真正的old對象,不會被回收 -- 作為老對象需要2次gc過程,作為新創建的對象需要經過3次gc才會到達此階段。

  G_TOUCHED1:標記位G_OLD的對象在這次gc barrier_back的狀態 -- 新touch的對象,需要進入到grayagain中

  G_TOUCHED2:標記為G_OLD的對象在上一次gc barrier_back的狀態前進到touched2 -- 分代gc結束時,從G_TOUCHED1轉成G_TOUCHED2,並設置為黑色,仍然存在於grayagain中,以便下一輪再次有新對象使該對象 barrier_back 時,只需要修改為灰色(使用計算機的局部性原理,這次用到的東西,下一次可能還會觸發,避免下次觸發時進行鏈表的操作)。如果下一次沒有觸發為touched1則變成G_OLD。在correctgraylist中會對touched2跟old的對象從grayagain刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM