应优先用 nativememory.alloc 替代 marshal.allochglobal:当需原生内存、严格指针对齐(8 字节)、统一 outofmemoryexception 异常、完全脱离 gc 且跨平台语义可控时;否则首选 arraypool 或 span。

NativeMemory 是 .NET 5+ 提供的轻量级非托管内存分配接口,不走 GC 系统、无句柄、无 Finalizer,适合高性能短生命周期缓冲区。它不是“更高级的 Marshal.AllocHGlobal”,而是语义更明确、跨平台行为更可控的替代方案。
什么时候该用 NativeMemory.Alloc 而不是 Marshal.AllocHGlobal
你真正需要原生内存时,才用 NativeMemory;如果只是临时托管缓冲区,优先用 ArrayPool<byte>.Shared.Rent()</byte> 或 Span<byte></byte> 栈分配。
-
Marshal.AllocHGlobal在 Windows 上走 COM 堆管理器,可能返回非对齐地址(如 16 字节对齐),而NativeMemory.Alloc严格按sizeof(void*)(通常 8 字节)对齐,更适合底层协议解析或与 C API 交互 - 分配失败时,
Marshal.AllocHGlobal可能抛COMException,NativeMemory.Alloc统一抛OutOfMemoryException,异常处理路径更一致 -
Marshal.AllocHGlobal分配的内存可被GCHandle引用,NativeMemory分配的完全脱离 GC 系统——这意味着你不能把它传给任何期待“托管生命周期”的 API(比如某些序列化器内部会尝试 pin) - Linux/macOS 上
Marshal.AllocHGlobal底层调用malloc,但行为受 libc 实现影响;NativeMemory.Alloc直接调用mmap(MAP_ANONYMOUS|MAP_PRIVATE),更接近系统级语义
AllocZeroed 不是“可选优化”,而是安全刚需
除非你明确知道分配后立刻写满、且内容不涉密,否则不要用裸 Alloc。零初始化不是性能负担,而是防止敏感数据残留和未定义行为的关键步骤。
-
NativeMemory.AllocZeroed(4096)底层等价于calloc,在多数系统上享受“零页”优化——内核直接映射物理零页,不实际分配 RAM,直到首次写入 - 用
Alloc + Unsafe.InitBlock模拟清零,不仅多一次系统调用开销,还绕过零页机制,实测慢 2–3 倍(尤其小块内存) - 没有“部分清零”API:请求 4KB 就清零 4KB,不会多也不会少;别指望它帮你跳过前 16 字节 header
- 别在 tight loop 里反复调
AllocZeroed(256)——每次都是系统调用;改用预分配池或ArrayPool复用
释放时机错位是崩溃主因,不是“忘了 Free”那么简单
最常见错误不是漏掉 Free,而是在错误上下文中调用它:异步、异常分支、重复释放、或跨线程传递 IntPtr。
- 必须配对使用:每个
Alloc/AllocZeroed都要有且仅有一个NativeMemory.Free(ptr),且只能调一次 - 不能在
catch之外假设“一定成功”——例如try { ptr = Alloc(); DoWork(); } finally { Free(ptr); }是错的,因为Alloc失败时ptr未赋值,Free会崩;正确写法是声明IntPtr ptr = IntPtr.Zero,再在finally中判空释放 -
async方法里分配的IntPtr,不能在await后释放——线程可能已切换,且ptr生命周期无法由编译器保证;这类场景应改用MemoryOwner<byte></byte>或托管池 - 别把
IntPtr存进 class 字段或ConcurrentDictionary——它没所有权语义,GC 不管它,你得自己确保所有引用都失效后再Free
真正的难点不在语法,而在判断“这块内存是否真的需要脱离 GC”。如果你还在犹豫要不要用 NativeMemory,大概率不该用——先测基线,确认 GC 压力来自分配频次而非单次大小,再考虑它。否则,ArrayPool 和 stackalloc 覆盖了 95% 的真实需求。










