固定对象时的 GC 行为

本文关键字:GC 行为 对象 | 更新日期: 2023-09-27 18:30:17

在浏览mscorlibPinnableObjectCache代码时,我遇到了以下代码:

for (int i = 0; i < m_restockSize; i++)
{
    // Make a new buffer.
    object newBuffer = m_factory();
    // Create space between the objects.  We do this because otherwise it forms 
    // a single plug (group of objects) and the GC pins the entire plug making 
    // them NOT move to Gen1 and Gen2. By putting space between them
    // we ensure that object get a chance to move independently (even if some are pinned).  
    var dummyObject = new object();
    m_NotGen2.Add(newBuffer);
}

这让我想知道对插头的引用是什么意思?尝试将对象固定在内存中时,GC 不会固定为该对象指定的特定地址吗?这种plug行为实际上在做什么,为什么需要在对象之间"间隔"

固定对象时的 GC 行为

好的,所以在多次尝试从具有"内部知识"的人那里获得官方回复后,我决定自己做一些实验。

试图做的是重现我有几个固定对象和它们之间一些未固定对象的场景(我使用了byte[])来尝试创建未固定的对象不会移动 GC 堆内更高一代的效果。

该代码在我的英特尔酷睿 i5 笔记本电脑上运行,在一个 32 位控制台应用程序中运行 Visual Studio 2015 的调试和发布中。我使用 WinDBG 实时调试了代码。

代码相当简单:

private static void Main(string[] args)
{
    byte[] byteArr1 = new byte[4096];
    GCHandle obj1Handle = GCHandle.Alloc(byteArr1 , GCHandleType.Pinned);
    object byteArr2 = new byte[4096];
    GCHandle obj2Handle = GCHandle.Alloc(byteArr2, GCHandleType.Pinned);
    object byteArr3 = new byte[4096];
    object byteArr4 = new byte[4096];
    object byteArr5 = new byte[4096];
    GCHandle obj4Handle = GCHandle.Alloc(byteArr5, GCHandleType.Pinned);
    GC.Collect(2, GCCollectionMode.Forced);
}

我首先使用以下!eeheap -gc查看 GC 堆地址空间:

generation 0 starts at 0x02541018 
generation 1 starts at 0x0254100c
generation 2 starts at 0x02541000 
ephemeral segment allocation context: none
segment      begin      allocated   size 
02540000     02541000   02545ff4    0x4ff4(20468)

现在,我逐步完成运行的代码,并观察对象的分配:

0:000> !dumpheap -type System.Byte[]
Address     MT          Size
025424e8    72101860    4108     
025434f4    72101860    4108     
02544500    72101860    4108     
0254550c    72101860    4108     
02546518    72101860    4108  

查看地址,我可以看到它们目前都是第 0 代,因为它从 0x02541018 开始。我还看到对象是使用 !gchandles 固定的:

Handle     Type      Object      Size    Data Type  
002913e4   Pinned    025434f4    4108    System.Byte[]
002913e8   Pinned    025424e8    4108    System.Byte[]

现在,我逐步完成代码,直到我到达运行GC.Collect行:

0:000> p
eax=002913e1 ebx=0020ee54 ecx=00000002 edx=00000001 esi=025424d8 edi=0020eda0
eip=0062055e esp=0020ed6c ebp=0020edb8 iopl=0  nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b  efl=00000206
0062055e e80d851272      call    mscorlib_ni+0xa28a70 (GC.Collect) (72748a70)

现在,预测会发生什么,我使用 !eeheap -gc 再次检查 GC 生成地址,并看到以下内容:

Number of GC Heaps: 1
generation 0 starts at 0x02547524
generation 1 starts at 0x0254100c
generation 2 starts at 0x02541000

第 0 代的起始地址已从 0x02541018 移至 0x02547524。现在,我检查固定和未固定byte[]对象的地址:

0:000> !dumpheap -type System.Byte[]
Address  MT           Size
025424e8 72101860     4108     
025434f4 72101860     4108     
02544500 72101860     4108     
0254550c 72101860     4108     
02546518 72101860     4108   

我看到他们住在同一个地址。但是,第 0 代现在从 0x02547524 开始的事实意味着他们都已晋升为第 1 代。

然后,我记得在Pro .NET Performance一书中读到过有关该行为的内容,其中指出以下内容:

固定对象可防止其被垃圾箱移动 收藏家。在代际模型中,它阻止了固定的提升 代际之间的对象。这在 年轻一代,比如0世代,因为规模 第 0 代非常小。导致碎片的固定对象 在第 0 代内有可能造成比它更大的伤害 可能出现在我们引入世代之前的检查固定 进入图片。幸运的是,CLR 有能力促进 使用以下技巧固定对象:如果第 0 代变为 使用固定对象严重碎片化,CLR 可以声明 第 0 代的整个空间被视为更高的一代和 从新的内存区域分配新对象,该区域将成为 第 0 代。这是通过更改临时段来实现的。

这实际上解释了我在WinDBG中看到的行为。

因此,总而言之,直到有人有任何其他解释,我认为该评论不正确,并且没有真正捕捉到GC内部真正发生的事情。如果有人有什么要详细说明的,我很乐意补充。