.netcore中的内存分配有哪些?它们之间有什么区别?
在 .NET 中,提供高性能、非托管或可控内存分配的方式主要有以下几种,但它们之间存在关键区别:
-
stackalloc -
ArrayPool<T>.Shared -
Span<T>/Memory<T>(通常与上述方式结合使用) -
NativeMemory类 (用于本地内存分配) -
Marshal类 (特别是AllocHGlobal和CoTaskMemAlloc)
下面我们来详细解释它们之间的区别。
对比总结表
| 特性 | stackalloc | ArrayPool<T>.Shared | NativeMemory / Marshal | 常规 new T[] |
|---|---|---|---|---|
| 内存位置 | 栈(Stack) | 托管堆(预先分配的大数组) | 非托管堆(Unmanaged Heap) | 托管堆(Managed Heap) |
| 内存管理 | 自动(方法返回时释放) | 手动租借/归还(池) | 手动(必须显式释放) | 自动(GC 回收) |
| 安全风险 | 栈溢出(大内存) | 无(池化管理) | 内存泄漏(若忘记释放) | 无 |
| 大小限制 | 很小(约 1 MB,取决于栈深度) | 很大(通常可达 1GB) | 很大(受系统内存限制) | 很大(受 GC 和系统限制) |
| 性能 | 极高(无分配压力,无GC) | 高(避免GC,复用数组) | 高(但分配/释放成本高于栈) | 较低(有GC压力) |
| 适用范围 | 小型的、短暂的缓冲区 | 中大型的、频繁使用的临时缓冲区 | 与本地代码互操作、需要精确控制的大型内存 | 通用的、长期存在的数组 |
| 返回类型 | Span<T> (C# 7.2+) |
T[] 或 ArraySegment<T> |
void* 或 IntPtr |
T[] |
| 需要不安全上下文 | 是(C# 7.2 前);否(与 Span<T> 一起使用时) |
否 | 是(通常需要) | 否 |
详细区别与说明
1. stackalloc
-
核心特点:在栈上分配内存块。栈内存的分配和释放速度极快,因为它只是移动栈指针。
-
优点:完全没有垃圾回收(GC)压力,性能极致。
-
缺点:
-
栈空间非常有限。默认栈大小通常为 1-4 MB,分配过大内存(如
stackalloc int[100000])极易导致栈溢出(StackOverflowException),且此异常无法被捕获,会导致进程立即终止。 -
分配的内存的生命周期严格限定在所在方法的作用域内。方法返回后,内存自动失效。
-
-
适用场景:极其注重性能的热路径代码,且需要分配的缓冲区非常小(例如,处理一个算法中的临时数组,大小在几百字节到几KB之间)。
-
示例:
2. ArrayPool<T>.Shared
-
核心特点:它是一个托管数组的对象池。你从池中“租借”(Rent)一个数组,用完后“归还”(Return)给它。池会缓存这些数组以供后续复用。
-
优点:
-
避免GC:通过复用数组,大大减少了托管堆上的分配和垃圾回收次数。
-
安全:没有栈溢出风险。
-
可分配较大内存:池中的数组可以很大。
-
-
缺点:
-
需要手动管理:必须记得调用
Return方法归还数组,否则失去池化的意义,甚至可能导致问题(例如,租借的数组可能包含旧数据)。 -
性能略低于
stackalloc,因为它仍然涉及托管堆上的一个数组对象,但远优于每次都new T[]。
-
-
适用场景:需要频繁创建和销毁的中大型临时数组或缓冲区(例如,网络IO、文件流处理中的缓冲区)。
-
示例:
3. NativeMemory / Marshal 类
-
核心特点:在非托管堆上分配原生内存块。这部分内存完全在 GC 的管辖范围之外。
-
优点:
-
内存大小不受 GC 约束,生命周期完全由开发者控制。
-
与本地代码互操作的必备手段(例如,为 P/Invoke 调用准备结构体)。
-
-
缺点:
-
必须手动释放!忘记调用对应的
Free方法会导致内存泄漏。 -
使用指针,通常需要
unsafe上下文,增加了代码的复杂性。 -
分配和释放成本高于栈,低于或近似于托管堆。
-
-
适用场景:
-
与本地 API 进行互操作。
-
需要分配非常大且生命周期很长、不希望给 GC 带来压力的内存块。
-
-
示例:
4. Span<T> 和 Memory<T> 的作用
需要特别注意的是,Span<T> 和 Memory<T> 本身不是内存分配机制,而是提供了一种统一、安全的方式来访问各种背衬存储(Backing Store)上的连续内存。
-
Span<T>:可以指向stackalloc的内存、ArrayPool的数组、常规new的数组、非托管内存等。它是ref struct,所以只能存在于栈上,这使得它能安全地指向栈内存。 -
Memory<T>:类似于Span<T>,但它不是ref struct,所以可以放在堆上(例如,用于异步方法)。它不能指向栈内存(如stackalloc的结果)。
它们是将上述各种分配方式与现代 .NET 代码连接起来的桥梁,让你能用相似的 API 操作不同来源的内存。
总结与选择建议
| 你的需求 | 推荐方案 |
|---|---|
| 极小(KB级)、短暂、极致性能的缓冲区 | stackalloc(务必确保尺寸很小!) |
| 频繁使用的中大型临时缓冲区 | ArrayPool<T>.Shared(首选,安全高效) |
| 与本地代码交互或完全控制生命周期的大型内存 | NativeMemory / Marshal(记得手动释放) |
| 普通用途的数组 | new T[](最简单,但GC有压力) |
简单来说,stackalloc 是性能最高但限制最大的特殊工具。在大多数需要优化临时缓冲区分配的场景下,ArrayPool<T>.Shared 是更通用、更安全的选择。而与非托管世界打交道时,则必须使用 NativeMemory 或 Marshal 类。

浙公网安备 33010602011771号