CoreCLR源碼探索(五) GC內存收集器的內部實現 調試篇


在上一篇中我分析了CoreCLR中GC的內部處理,
在這一篇我將使用LLDB實際跟蹤CoreCLR中GC,關於如何使用LLDB調試CoreCLR的介紹可以看:

  • 微軟官方的文檔,地址
  • 我在第3篇中的介紹,地址
  • LLDB官方的入門文檔,地址

源代碼

本篇跟蹤程序的源代碼如下:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication
{
    public class Program
    {
        public class ClassA { }
        public class ClassB { }
        public class ClassC { }
        
        public static void Main(string[] args)
        {
            var a = new ClassA();
            { var b = new ClassB(); }
            var c = new ClassC();
            
            GCHandle handle = GCHandle.Alloc(c, GCHandleType.Pinned);
            IntPtr address = handle.AddrOfPinnedObject();
            Console.WriteLine((long)address);
            
            GC.Collect();
            Console.WriteLine("first collect completed");
            
            c = null;
            GC.Collect();
            Console.WriteLine("second collect completed");
            
            GC.Collect();
            Console.WriteLine("third collect completed");
        }
    }
}

准備調試

環境和我的第三篇文章一樣,都是ubuntu 16.04 LTS,首先需要發布程序:

dotnet publish

發布程序后,把自己編譯的coreclr文件覆蓋到發布目錄中:
復制coreclr/bin/Product/Linux.x64.Debug下的文件到程序目錄/bin/Debug/netcoreapp1.1/ubuntu.16.04-x64/publish下。
請不要設置開啟服務器GC,一來是這篇文章分析的是工作站GC的處理,二來開啟服務器GC很容易導致調試時死鎖。

進入調試

准備工作完成以后就可以進入調試了

cd 程序目錄/bin/Debug/netcoreapp1.1/ubuntu.16.04-x64/publish
lldb-3.6 程序名稱

首先設置gc主函數的斷點,然后運行程序

b gc1
r

我們停在了gc1函數,現在可以用bt來看調用來源

這次是手動觸發GC,調用來源中包含了GCInterface::Collect和JIT生成的函數

需要顯示當前的本地變量可以用fr v,需要打印變量或者表達式可以用p

現在用n來步過,用s來步進繼續跟蹤代碼

進入標記階段

在上圖的位置中用s命令即可進入mark_phase,繼續步過到下圖的位置

這時先讓我們看下堆中的對象,加載CoreCLR提供的LLDB插件

plugin load libsosplugin.so

插件提供的命令可以查看這里的文檔

執行dumpheap查看堆中的狀態


執行dso查看堆和寄存器中引用的對象

執行dumpobj查看對象的信息

在這一輪gc中對象a b c都會存活下來,
可能你會對為什么b能存活下來感到驚訝,對象b的引用分配在棧上,即時生命周期過了也不一定會失效(rsp不會移回去)

br s -n Promote -c "(long)*ppObject == 0x00007fff5c01a2b8" # -n 名稱 -c 條件
c # 繼續執行

接下來步進mark_object_simple函數,然后步進gc_mark1函數

me re -s8 -c3 -fx o # 顯示地址中的內存,8個字節一組,3組,hex格式,地址是o
p ((CObjectHeader*)o)->IsMarked() # 顯示對象是否標記存活

我們可以清楚的看到標記對象存活設置了MethodTable的指針|= 1

現在給PinObject下斷點

br s -n PinObject -c "(long)*pObjRef == 0x00007fff5c01a1a0"
c

可以看到只是調用Promote然后傳入GC_CALL_PINNED

繼續步進到if (flags & GC_CALL_PINNED)下的pin_object

可以看到pinned標記設置在同步索引塊中

進入計划階段

進入計划階段后首先打印一下各個代的狀態

p generation_table

使用這個命令可以看到gen 0 ~ gen 3的狀態,最后一個元素是空元素不用在意

繼續步過下去到下圖的這一段

在這里我們找到了一個plug的開始,然后枚舉已標記的對象,下圖是擦除marked和pinned標記的代碼

在這里我們找到了一個plug的結束

如果是Full GC或者不升代,在處理第一個plug之前就會設置gen 2的計划代邊界

模擬壓縮的地址

如果x越過原來的gen 0的邊界,設置gen 1的計划代邊界(原gen 1的對象變gen 2),
如果不升代這里也會設置gen 0的計划代邊界

模擬壓縮后把原地址與壓縮到的地址的偏移值存到plug信息(plug前的一塊內存)中

構建plug樹

設置brick表,這個plug樹跨了6個brick


如果升代,模擬壓縮全部完成后設置gen 0的計划代邊界

接下來如果不動里面的變量,將會進入清掃階段(不滿足進入壓縮階段的條件)

進入清掃階段

這次為了觀察對象c如何被清掃,我們進入第二次gc的make_free_lists

b make_free_lists
c

處理當前brick中的plug樹

前面看到的對象c的地址是0x00007fff5c01a2e8,這里我們就看對象c后面的plug是如何處理的

br s -f gc.cpp -l 23070 -c "(long)tree > 0x00007fff5c01a2e8"
c

我們可以看到plug 0x00007fff5c01a300前面的空余空間中包含了對象c,空余空間的開始地址就是對象c

接下來就是在這片空余空間中創建free object和加到free list了,
這里的大小不足(< min_free_list)所以只會創建free object不會加到free list中

設置代邊界,之前計划階段模擬的計划代邊界不會被使用

清掃階段完成后這次的gc的主要工作就完成了,接下來讓我們看重定位階段和壓縮階段

進入重定位階段

使用上面的程序讓計划階段選擇壓縮,需要修改變量,這里重新運行程序並使用以下命令

b gc.cpp:22489
c
expr should_compact = true

n步過到下圖的位置,s步進到relocate_phase函數

到這個位置可以看到用了和標記階段一樣的GcScanRoots函數,但是傳入的不是Promote而是Relocate函數

接下來下斷點進入Relocate函數

b Relocate
c

GCHeap::Relocate函數不會重定位子對象,只是用來重定位來源於根對象的引用

一直走到這個位置然后進入gc_heap::relocate_address函數

根據原地址和brick table找到對應的plug樹

搜索plug樹中old_address所屬的plug

根據plug中的reloc修改指針地址

現在再來看relocate_survivors函數,這個函數用於重定位存活下來的對象中的引用

b relocate_survivors
c

接下來會枚舉並處理brick,走到這里進入relocate_survivors_in_brick函數,這個函數處理單個brick中的plug樹

遞歸處理plug樹種的各個節點

走到這里進入relocate_survivors_in_plug函數,這個函數處理單個plug中的對象

圖中的這個plug結尾被下一個plug覆蓋過,需要特殊處理,這里繼續進入relocate_shortened_survivor_helper函數

當前是unpinned plug,下一個plug是pinned plug

枚舉處理plug中的各個對象

如果這個對象結尾未被覆蓋,則調用relocate_obj_helper重定位對象中的各個成員


如果對象結尾被覆蓋了,則調用relocate_shortened_obj_helper重定位對象中的各個成員
在這里成員如果被覆蓋會調用reloc_ref_in_shortened_obj修改備份數據中的成員,但是因為go_through_object_nostart是一個macro這里無法調試內部的代碼

接下來我們觀察對象a的地址是否改變了

重新運行並修改should_compact變量

b gc.cpp:22489
r
expr should_compact = true
plugin load libsosplugin.so
dso

我們可以看到對象a的地址在0x00007fff5c01a2b8,接下來給relocate_address函數下斷點

br s -n relocate_address -c "(long)(*pold_address) == 0x00007fff5c01a2b8"
c

我們可以看到地址由0x00007fff5c01a2b8變成了0x00007fff5c0091b8

接下來一直跳回plan_phase,下圖可以看到重定位階段完成以后新的地址上仍無對象,重定位階段只是修改了地址並未復制內存,直到壓縮階段完成以后對象才會在新的地址

接下來看壓縮階段

進入壓縮階段

在重定位階段完成以后走到下圖的位置,步進即可進入壓縮階段

枚舉brick table

處理單個brick table中的plug樹

根據下一個tree的gap計算last_plug的大小

處理單個plug中的對象

上面的last_plug是pinned plug所以不移動,這里找了另外一個會移動的plug

下圖可以看到整個plug都被復制到新的地址

這里再找一個結尾被覆蓋過的plug看看是怎么處理的

首先把被覆蓋的結尾大小加回去

然后把被覆蓋的內容臨時恢復回去


復制完再把覆蓋的內容交換回來,因為下一個plug還需要用

最終在recover_saved_pinned_info會全部恢復回去

參考鏈接

https://github.com/dotnet/coreclr/blob/master/Documentation/botr/garbage-collection.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/linux-instructions.md
https://github.com/dotnet/coreclr/blob/release/1.1.0/Documentation/building/debugging-instructions.md
http://lldb.llvm.org/tutorial.html
http://lldb.llvm.org/lldb-gdb.html

寫在最后

這一篇中我列出了幾個gc中比較關鍵的部分,但是還有成千上百處可以探討的部分,
如果你有興趣可以自己試着用lldb調試CoreCLR,可以學到很多文檔和書籍之外的知識,
特別是對於CoreCLR這種文檔少注釋也少的項目,掌握調試工具可以大幅減少理解代碼所需的時間

寫完這一篇我將暫停研究GC,下一篇開始會介紹JIT相關的內容,敬請期待


免責聲明!

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



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