GCHandle的使用

技术博客

Posted by 秋蜩鸣北林 on January 10, 2026

GCHandle 是 .NET 提供的结构体,用于在托管与非托管代码之间管理托管对象的生命周期和内存位置。主要功能:

  1. 防止垃圾回收:确保对象在句柄释放前不被 GC 回收
  2. 固定对象内存位置:使用 GCHandleType.Pinned 固定对象,防止 GC 移动对象,从而获取稳定的内存地址

内部实现机制

GCHandle 通过内部句柄表实现:

  • 句柄表:维护一个全局句柄表,每个句柄包含:
    • 对托管对象的引用
    • 类型标志(Normal、Pinned、Weak、WeakTrackResurrection 等)
  • 句柄分配:GCHandle.Alloc() 在句柄表中创建条目,返回句柄值
  • 句柄释放:Free() 从表中移除条目,允许 GC 正常回收对象

必须使用 GCHandle 的场景

1. 托管与非托管代码互操作(P/Invoke)

需要将托管对象的指针传递给非托管代码时,必须固定对象:

using System;
using System.Runtime.InteropServices;

[DllImport("native.dll")]
static extern void ProcessData(IntPtr data, int length);

void Example()
{
    byte[] buffer = new byte[1024];
  
    // 必须固定对象,否则GC可能移动或回收它
    GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    try
    {
        IntPtr ptr = handle.AddrOfPinnedObject();
        ProcessData(ptr, buffer.Length);
    }
    finally
    {
        handle.Free(); // 必须释放,否则内存泄漏
    }
}

2. 非托管代码回调托管代码

非托管代码需要回调托管方法,且需要传递托管对象上下文时:

// 非托管回调函数签名
delegate void CallbackDelegate(IntPtr userData);

[DllImport("native.dll")]
static extern void RegisterCallback(CallbackDelegate callback, IntPtr userData);

void Example()
{
    // 托管对象
    MyClass obj = new MyClass();
  
    // 创建句柄,将托管对象转换为IntPtr
    GCHandle handle = GCHandle.Alloc(obj);
    IntPtr userData = GCHandle.ToIntPtr(handle);
  
    RegisterCallback(MyCallback, userData);
}

static void MyCallback(IntPtr userData)
{
    // 在回调中恢复对象
    GCHandle handle = GCHandle.FromIntPtr(userData);
    MyClass obj = (MyClass)handle.Target;
    // 使用obj...
}

3. 需要获取对象固定内存地址

需要直接访问对象内存地址的场景(如直接内存操作、零拷贝等):

byte[] data = new byte[1000];
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
IntPtr address = handle.AddrOfPinnedObject();

// 现在可以安全地使用address,对象不会被移动
// 使用完毕后必须调用 handle.Free()

重要注意事项

  1. 必须释放:使用后必须调用 Free(),否则会导致内存泄漏
  2. 固定对象的影响:Pinned 类型会阻止 GC 移动对象,可能增加内存碎片,应谨慎使用
  3. 性能考虑:固定对象会影响 GC 性能,应尽快释放
  4. 由于GCHandle是结构体,一定要谨慎地进行赋值操作(尽量避免),下面就是个bad case:
MyClass obj = new MyClass();

// 创建句柄,将托管对象转换为IntPtr
GCHandle handle = GCHandle.Alloc(obj);

// ...
// 在之后的某个地方
// 把handle传递给了tmp
var tmp = handle;
tmp.Free() // 这里确实会把 obj 对象释放掉,但是会导致 handle 内部异常(地址对应的对象已被释放,null reference exception)