在上一篇中我分析了CoreCLR中GC的內部處理,
在這一篇我將使用LLDB實際跟蹤CoreCLR中GC,關於如何使用LLDB調試CoreCLR的介紹可以看:
源代碼
本篇跟蹤程序的源代碼如下:
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相關的內容,敬請期待