Memory 是 .NET Core 2.1+ 引入的轻量级内存抽象,用于表示可读写的连续内存视图,支持跨 async 边界传递;适用于高性能 I/O 缓冲复用、序列化中间层等场景,但不可替代 List 或长期持有数据。

Memory 是什么,什么时候该用它
Memory<t></t> 是 .NET Core 2.1+ 引入的轻量级内存抽象,用于表示一段可读写的连续内存(可以是托管数组、堆栈内存、本机内存等)。它本身不拥有内存,只持有“视图”——类似 Span<t></t>,但支持跨 async 边界传递(Span<t></t> 不能作为字段或异步方法参数)。
适合场景包括:高性能 I/O 缓冲区复用、序列化/反序列化中间层、零拷贝字符串处理、自定义协议解析。不适合直接替代 List<t></t> 或长期持有数据。
如何从常见来源创建 Memory创建方式决定底层内存生命周期和性能特征,选错容易引发 ObjectDisposedException 或意外 GC 压力:
- 从数组:
Memory<t> mem = new byte[1024];</t> → 安全,但数组仍受 GC 管理
- 从堆栈分配(仅限
Span<t></t>,需转为 Memory<t></t> 时注意):Span<byte> stackSpan = stackalloc byte[256]; Memory<byte> mem = stackSpan;</byte></byte> → 仅限同步本地作用域,不可传出方法
- 从
ArrayPool<byte>.Shared.Rent()</byte>:Memory<byte> mem = ArrayPool<byte>.Shared.Rent(4096);</byte></byte> → 推荐用于高频短时缓冲,记得 .Return()
- 从
UnmanagedMemoryStream 或 NativeMemory.Allocate() → 需手动管理生命周期,易出错,慎用
切片、复制与避免隐式分配
Memory<t></t> 的 Slice() 是 O(1) 操作,不复制数据;但 ToArray() 或 ToString()(对 Memory<char></char>)会触发堆分配,破坏零拷贝目标:
- 安全切片:
Memory<byte> header = packet.Slice(0, 4);</byte>
- 危险复制:
byte[] copy = packet.ToArray(); → 新数组,GC 压力
- 替代方案:
var reader = new BinaryReader(packet.AsStream()); → 复用流接口,避免中间数组
- 写入时优先用
Span<t></t> 方法:mem.Span.Fill(0);、mem.Span.CopyTo(dest.Span);
异步方法中使用 Memory 的关键约束
Memory<t></t> 可作为 async 方法参数/返回值,但必须确保其背后内存在整个异步操作期间有效。常见陷阱:
- 不要把局部
stackalloc 转成 Memory<t></t> 后传入 await → 堆栈帧已销毁
- 不要在
using var array = new byte[...] 块内创建 Memory<byte></byte> 并 await → array 可能被回收
- 推荐模式:
Memory<byte> buffer = ArrayPool<byte>.Shared.Rent(size); try { await stream.ReadAsync(buffer); } finally { ArrayPool<byte>.Shared.Return(buffer); }</byte></byte></byte>
- 若需长期持有,考虑
MemoryManager<t></t> 自定义实现,但复杂度陡增
真正难的不是调用 Memory<t></t> 的 API,而是判断哪段内存归谁管、生命周期是否对齐、以及何时该退回到 ArrayPool + Span<t></t> 组合。多数性能问题其实出在“以为用了 Memory<t></t> 就自动高效”,而忽略了背后内存的归属和释放时机。
Memory<t></t> 是 .NET Core 2.1+ 引入的轻量级内存抽象,用于表示一段可读写的连续内存(可以是托管数组、堆栈内存、本机内存等)。它本身不拥有内存,只持有“视图”——类似 Span<t></t>,但支持跨 async 边界传递(Span<t></t> 不能作为字段或异步方法参数)。
适合场景包括:高性能 I/O 缓冲区复用、序列化/反序列化中间层、零拷贝字符串处理、自定义协议解析。不适合直接替代 List<t></t> 或长期持有数据。
如何从常见来源创建 Memory创建方式决定底层内存生命周期和性能特征,选错容易引发 ObjectDisposedException 或意外 GC 压力:
- 从数组:
Memory<t> mem = new byte[1024];</t> → 安全,但数组仍受 GC 管理
- 从堆栈分配(仅限
Span<t></t>,需转为 Memory<t></t> 时注意):Span<byte> stackSpan = stackalloc byte[256]; Memory<byte> mem = stackSpan;</byte></byte> → 仅限同步本地作用域,不可传出方法
- 从
ArrayPool<byte>.Shared.Rent()</byte>:Memory<byte> mem = ArrayPool<byte>.Shared.Rent(4096);</byte></byte> → 推荐用于高频短时缓冲,记得 .Return()
- 从
UnmanagedMemoryStream 或 NativeMemory.Allocate() → 需手动管理生命周期,易出错,慎用
切片、复制与避免隐式分配
Memory<t></t> 的 Slice() 是 O(1) 操作,不复制数据;但 ToArray() 或 ToString()(对 Memory<char></char>)会触发堆分配,破坏零拷贝目标:
- 安全切片:
Memory<byte> header = packet.Slice(0, 4);</byte>
- 危险复制:
byte[] copy = packet.ToArray(); → 新数组,GC 压力
- 替代方案:
var reader = new BinaryReader(packet.AsStream()); → 复用流接口,避免中间数组
- 写入时优先用
Span<t></t> 方法:mem.Span.Fill(0);、mem.Span.CopyTo(dest.Span);
异步方法中使用 Memory 的关键约束
Memory<t></t> 可作为 async 方法参数/返回值,但必须确保其背后内存在整个异步操作期间有效。常见陷阱:
- 不要把局部
stackalloc 转成 Memory<t></t> 后传入 await → 堆栈帧已销毁
- 不要在
using var array = new byte[...] 块内创建 Memory<byte></byte> 并 await → array 可能被回收
- 推荐模式:
Memory<byte> buffer = ArrayPool<byte>.Shared.Rent(size); try { await stream.ReadAsync(buffer); } finally { ArrayPool<byte>.Shared.Return(buffer); }</byte></byte></byte>
- 若需长期持有,考虑
MemoryManager<t></t> 自定义实现,但复杂度陡增
真正难的不是调用 Memory<t></t> 的 API,而是判断哪段内存归谁管、生命周期是否对齐、以及何时该退回到 ArrayPool + Span<t></t> 组合。多数性能问题其实出在“以为用了 Memory<t></t> 就自动高效”,而忽略了背后内存的归属和释放时机。
创建方式决定底层内存生命周期和性能特征,选错容易引发 ObjectDisposedException 或意外 GC 压力:
- 从数组:
Memory<t> mem = new byte[1024];</t>→ 安全,但数组仍受 GC 管理 - 从堆栈分配(仅限
Span<t></t>,需转为Memory<t></t>时注意):Span<byte> stackSpan = stackalloc byte[256]; Memory<byte> mem = stackSpan;</byte></byte>→ 仅限同步本地作用域,不可传出方法 - 从
ArrayPool<byte>.Shared.Rent()</byte>:Memory<byte> mem = ArrayPool<byte>.Shared.Rent(4096);</byte></byte>→ 推荐用于高频短时缓冲,记得.Return() - 从
UnmanagedMemoryStream或NativeMemory.Allocate()→ 需手动管理生命周期,易出错,慎用
切片、复制与避免隐式分配
Memory<t></t> 的 Slice() 是 O(1) 操作,不复制数据;但 ToArray() 或 ToString()(对 Memory<char></char>)会触发堆分配,破坏零拷贝目标:
- 安全切片:
Memory<byte> header = packet.Slice(0, 4);</byte> - 危险复制:
byte[] copy = packet.ToArray();→ 新数组,GC 压力 - 替代方案:
var reader = new BinaryReader(packet.AsStream());→ 复用流接口,避免中间数组 - 写入时优先用
Span<t></t>方法:mem.Span.Fill(0);、mem.Span.CopyTo(dest.Span);
异步方法中使用 Memory 的关键约束
Memory<t></t> 可作为 async 方法参数/返回值,但必须确保其背后内存在整个异步操作期间有效。常见陷阱:
- 不要把局部
stackalloc 转成 Memory<t></t> 后传入 await → 堆栈帧已销毁
- 不要在
using var array = new byte[...] 块内创建 Memory<byte></byte> 并 await → array 可能被回收
- 推荐模式:
Memory<byte> buffer = ArrayPool<byte>.Shared.Rent(size); try { await stream.ReadAsync(buffer); } finally { ArrayPool<byte>.Shared.Return(buffer); }</byte></byte></byte>
- 若需长期持有,考虑
MemoryManager<t></t> 自定义实现,但复杂度陡增
真正难的不是调用 Memory<t></t> 的 API,而是判断哪段内存归谁管、生命周期是否对齐、以及何时该退回到 ArrayPool + Span<t></t> 组合。多数性能问题其实出在“以为用了 Memory<t></t> 就自动高效”,而忽略了背后内存的归属和释放时机。
Memory<t></t> 可作为 async 方法参数/返回值,但必须确保其背后内存在整个异步操作期间有效。常见陷阱:
- 不要把局部
stackalloc转成Memory<t></t>后传入await→ 堆栈帧已销毁 - 不要在
using var array = new byte[...]块内创建Memory<byte></byte>并 await →array可能被回收 - 推荐模式:
Memory<byte> buffer = ArrayPool<byte>.Shared.Rent(size); try { await stream.ReadAsync(buffer); } finally { ArrayPool<byte>.Shared.Return(buffer); }</byte></byte></byte> - 若需长期持有,考虑
MemoryManager<t></t>自定义实现,但复杂度陡增
真正难的不是调用 Memory<t></t> 的 API,而是判断哪段内存归谁管、生命周期是否对齐、以及何时该退回到 ArrayPool + Span<t></t> 组合。多数性能问题其实出在“以为用了 Memory<t></t> 就自动高效”,而忽略了背后内存的归属和释放时机。


















