当固定一个对象时GC的行为

37

在浏览 mscorlib 中的 PinnableObjectCache 的代码时,我遇到了以下代码:

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);
}

这让我想知道“插头”(plug)的引用是什么意思?在尝试将对象固定在内存中时,垃圾回收不会将特定地址指定为对象的固定地址吗?实际上,这个“插头”行为是在做什么,为什么需要在对象之间“留出空隙”?


博客文章http://blogs.msdn.com/b/maoni/archive/2005/10/03/so-what-s-new-in-the-clr-2-0-gc.aspx中有更多信息,请向下滚动至“碎片控制”部分。但我不确定它是否回答了问题?! - Matt Warren
1
@MattWarren提到了降级,其中在固定对象之间的对象不会被提升。但在这个例子中,作者故意在固定对象之间分配了一个空间,以确保它们独立地被提升。不幸的是,它并没有涉及插头行为:\ - Yuval Itzchakov
3
这个评论是由一个新手微软程序员写的,他只有一半的头绪。部分准确,他被要求找到解决方法的问题是真实存在的。是的,在GC紧凑算法中存在插头和间隙,并且在分配可能被固定的多个缓冲区时,这是需要担心的问题。那些夹在中间的可能在GC运行时被取消固定,但不会移动。其余的评论不是很准确。这是破解代码,出现两次会失去一千个优雅点,但它很无害,并尝试解决核心问题。 - Hans Passant
2
@HansPassant,您可否在答案中更详细地阐述垃圾回收的行为? - Yuval Itzchakov
如果我正确理解上面的代码,那么它意味着GC可能会意外地固定那些没有设置为固定的对象,如果它们被夹在固定的对象之间。这意味着夹在中间的对象将无法随着其生命周期的增长而向不同的GC代移动。我不认为这是正确的。Maoni的幻灯片清楚地表明仅固定的对象具有不向上移动到Gens的效果,并且没有提到意外固定这些“插头”。老实说,我认为这只是遗忘的一行代码。如果你找到了测试的方法,请告诉我们。 - Alexandru
显示剩余9条评论
1个回答

19

好的,所以在多次试图从“内部人士”获得官方答复之后,我决定自己进行一些实验。

我尝试的是再现这样一种情景,即我有几个固定的对象和一些未固定的对象在它们之间(我使用了一个 byte[]),以尝试创建这样一种效果:未固定的对象不会移动到GC堆内的较高代中。

代码在我的Intel Core i5笔记本电脑上运行,在运行Visual Studio 2015的32位控制台应用程序中以Debug和Release方式运行。 我使用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。 现在,我检查了被固定(pinned)和未被固定(none pinned)的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内部发生的情况。如果有人有任何要详细说明的内容,我很乐意添加。


抱歉如果这是一个愚蠢的问题,但是obj1是什么?它应该是byteArr1吗? - dumbledad
@dumbledad 你说得完全正确。我对代码进行了几次迭代,忘记在答案中重命名变量了。 - Yuval Itzchakov
你应该在调用GC之后添加一些内容来保持对象的存活,因为我认为只有调试器才能使它们保持存活。 - Ian Ringrose
1
@Ian 我不确定我理解了。问题的重点是想看到在 GC 收集期间,当一些对象被固定而其他对象没有被固定时会发生什么,无论未固定的对象是否存活。 - Yuval Itzchakov
如果在调用GCHandle.Alloc(byteArr5, GCHandleType.Pinned)之前,byteArr3和byteArr4被垃圾回收了怎么办?(我只是不想做那些不是100%可重复的实验....) - Ian Ringrose
@IanRingrose 从 dumpheap 命令中可以看出它们没有被回收。这个测试的目的是检查当发生分配时 固定 对象会发生什么。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接