GCHandle 是 .NET 提供的结构体,用于在托管与非托管代码之间管理托管对象的生命周期和内存位置。主要功能:
- 防止垃圾回收:确保对象在句柄释放前不被 GC 回收
- 固定对象内存位置:使用
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()
重要注意事项
- 必须释放:使用后必须调用
Free(),否则会导致内存泄漏 - 固定对象的影响:
Pinned类型会阻止 GC 移动对象,可能增加内存碎片,应谨慎使用 - 性能考虑:固定对象会影响 GC 性能,应尽快释放
- 由于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)