C# 中固定对象(GCHandle)如何影响 GC 与并发性能?

来源:网站主作者:梁博渊头衔:网络博主
导读:本期聚焦于梁博渊创作的《C# 中固定对象(GCHandle)如何影响 GC 与并发性能?》,敬请观看详情。C# 的 GCHandle 固定机制虽然能让托管对象地址稳定,但会打乱 GC 的压缩计划,导致堆碎片增多,并在后台 GC 阶段引入额外扫描开销。本文从固定句柄的分配方式入手,解释 Pinned 与 Normal、Weak 的区别,结合一个简单但典型的物理文件读写场景,展示固定操作如何在 Gen0、Gen1 晋升过程中制造不可移动的“钉子”。随后重点分析固定对象对工作站并发 GC 和服务器 GC 的暂停时间、工作线程内存分配竞争的不同影响,并用 BenchmarkDotNet 微型基准对比 fixed 语句、GCHandle 常驻固定与 ArrayPool 非托管缓冲三种方案。最后给出四条可落地的优化建议,包括缩短固定生命周期、优先使用栈上 fixed、改用非托管内存或池化数组。文章希望帮读者在需要与非托管代码交互时,更清醒地权衡固定对象的代价。

C# 的垃圾回收器在回收过程中通常会移动存活对象,以便合并空闲空间,让后续分配只需移动分配指针即可完成。然而,当托管对象需要被传递给非托管函数,或者被 unsafe 代码直接操作时,对象的地址必须保持稳定,否则原生代码里保存的指针就会变成悬空指针。GCHandle.Alloc 配合 GCHandleType.Pinned 正是为此设计:它创建一个固定句柄,阻止 GC 移动目标对象。固定操作虽然简单,但它会改变 GC 对堆的整理策略,并增加并发回收时的扫描与调度成本。本文会从固定句柄的底层行为讲起,逐步拆解它对垃圾回收效率和多线程分配性能的影响。

C# 中固定对象(GCHandle)如何影响 GC 与并发性能?

GCHandle 固定对象的底层机制

GCHandle 是 .NET 运行时提供的一种强引用包装。GCHandleType 有 Normal、Weak、WeakTrackResurrection 和 Pinned 四种模式。Normal 相当于普通强引用,Weak 不阻止回收,而 Pinned 不仅保持对象存活,还会把对象标记为不可移动。创建固定句柄后,可以通过 AddrOfPinnedObject 获得对象在托管堆上的稳定地址,也可以将该地址转换为指针后传给非托管代码。

固定对象的实现并不只是设置一个标志位。GC 在标记和压缩阶段会遍历所有固定句柄,把句柄引用的对象当作根。压缩阶段计算对象新地址时,凡是命中固定句柄的对象都不能移动。对于小型对象堆(SOH)来说,一个固定对象就像钉在内存段里的一颗钉子:它前面的空闲空间无法被后续对象填充,它后面的对象也不能向前挤压,只能绕过它去寻找新的连续空间。

固定对象的地址虽然稳定,但它在代际提升上仍然遵循普通规则。一个 Gen0 对象如果被固定时间较长,它可能被提升到 Gen1 甚至 Gen2。一旦高代区域里存在大量固定对象,GC 在决定是否进行完全压缩时会更加困难,碎片化也会随着程序运行时间逐步累积。理解这一点,是分析固定对象对性能影响的前提。

固定对象对垃圾回收性能的影响

垃圾回收的效率很大程度上依赖于“压缩”操作。压缩会把存活对象移动到一段连续内存的起点,释放出完整空闲块。但如果堆中存在固定对象,压缩就必须以这些对象为边界,进行分段移动。结果就是原本一次线性扫描可以完成的工作,被拆成多个小区间;每个小区间之间留下的空洞无法合并,下一次分配时可能因为找不到足够大的连续空间而提前触发 GC。

固定对象还会影响分配指针的推进。现代 .NET 使用 bump pointer 分配策略,正常对象分配只需要移动指针。当堆碎片化严重时,CLR 需要在空闲列表里寻找合适的块,分配路径从 O(1) 退化为 O(n),并且多线程分配时对空闲列表的竞争也会增加。如果固定对象正好位于 Gen0 分配区域内,这个代价会被放大,因为 Gen0 的回收最频繁。

另一个容易被忽略的影响是固定句柄的释放时机。调用 Free 之后,对象并不会立刻恢复可移动状态,而是要等到下一次 GC 时重新计算。如果在两次 GC 之间频繁分配和释放固定句柄,运行时需要维护额外的句柄表,并产生句柄扫描成本。因此,即使固定对象本身很小,不必要的常驻 GCHandle 也可能拖累整体吞吐。

对并发 GC 与多线程分配的具体影响

工作站并发 GC 通常允许后台标记和清扫与用户线程同时进行,但由于压缩必须停止所有线程,固定对象越多,压缩阶段越难在短时间内完成。服务器 GC 虽然为每个逻辑处理器准备了独立堆,但固定对象仍然会破坏各自堆内的连续性。一旦某个处理器对应的堆出现严重碎片,分配线程不得不等待 GC,甚至发生“阻塞式回收”,导致吞吐量急剧下降。

在多线程环境中,固定句柄本身并不是线程安全的资源。如果多个线程同时调用 GCHandle.Alloc 或 Free,必须使用锁或其他同步机制,否则会破坏句柄表的内部状态。即便使用锁,固定句柄表也可能成为热点。更常见的情况是,多个线程同时固定不同对象并触发 GC,GC 需要扫描更多的固定句柄,后台标记工作可能被延长,用户线程的暂停时间随之增加。

代码示例展示了最典型的固定用法。下面这段代码在调用非托管 API 前固定一个托管字节数组,并在 finally 中释放句柄,避免异常路径导致泄漏。

byte[] buffer = new byte[4096];

GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
    IntPtr ptr = handle.AddrOfPinnedObject();
    // 调用 NativeMethods.ReadFromDevice(ptr, buffer.Length)
}
finally
{
    handle.Free();
}

这个写法正确但不够高效。如果 NativeMethods.ReadFromDevice 是一个高频率调用,反复分配和释放固定句柄会带来可观的句柄扫描成本。更推荐的方式是用 fixed 语句在栈上临时固定数组,fixed 语句会在方法执行期间阻止对象移动,离开作用域后自动恢复,开销通常低于 GCHandle。

替代方案与优化实践

如果数据只需要在极短时间内传递给非托管代码,优先使用 C# 的 fixed 语句。fixed 语句通过 IL 层的 pinned 局部变量实现固定,不需要分配单独句柄,生命周期由编译器保证。下面是一个使用 fixed 的示例,它适用于同步且短小的 P/Invoke 调用。

byte[] buffer = new byte[4096];

unsafe
{
    fixed (byte* ptr = buffer)
    {
        // 直接在栈上使用 ptr,避免 GCHandle 分配
    }
}

如果非托管调用是异步的,或者缓冲区需要跨多次回调存活,固定托管数组可能不合适。此时更推荐使用非托管内存或池化缓冲区。非托管内存不受 GC 管理,自然不存在对象移动问题;池化缓冲区虽然最终要归还,但可以在固定窗口内与 fixed 语句配合,减少频繁的大数组分配。下面是非托管内存的使用方式。

IntPtr unmanagedBuffer = Marshal.AllocHGlobal(4096);
try
{
    // 将 unmanagedBuffer 交给非托管代码
}
finally
{
    Marshal.FreeHGlobal(unmanagedBuffer);
}

对于需要频繁借用大块缓冲区的场景,可以使用 ArrayPool<byte>.Shared.Rent 租用缓冲区,配合固定语句短时固定,用完后 Return。这样既减少了分配压力,也把固定窗口压缩到最短。

最后要强调的是,任何固定操作都应该有明确的释放边界。如果确实需要长期固定对象,请固定尽量小的对象,并定期评估是否可以用非托管内存替代。固定对象不是免费的午餐,它换取的是地址稳定性,付出的代价是 GC 压缩能力下降、碎片增加以及并发回收时更长的暂停。理解这些影响,有助于在 P/Invoke、unsafe 代码和实时数据处理场景中做出更合理的取舍。

GCHandle固定对象并发性能修改时间:2026-08-26 15:58:18

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。