内存页
一、什么是“内存页”?
1. 内存页的定义
- 内存页(Page)是操作系统分配内存的最小单位,典型大小为 4KB(即 4096 字节)。
- 虚拟内存(程序看到的地址空间)和物理内存之间的映射就是以页为单位建立的。
你可以把物理内存理解成一张纸被裁成很多“页”,操作系统通过“页表”来记录这些页怎么分配。
2. 为什么要用“内存页”?
- 性能优化:如果数据能按页对齐分布,内存访问更高效。
- 内存映射:可以将文件直接映射到内存页上,进行快速读写。
- 跨进程共享内存:两个程序可以共享同一块内存页进行通信。
二、在 C# 中如何使用“内存页”?
我们重点掌握两种方式:
✅ 方式一:使用 MemoryMappedFile 映射一块内存页
这个方法最实用,适合用于大文件访问、多进程共享内存等场景。
👇 示例:创建一块 4KB 的共享内存
using System;
using System.IO.MemoryMappedFiles;
class Program
{
static void Main()
{
// 创建或打开一个内存映射文件,大小为一页(4096 字节)
using var mmf = MemoryMappedFile.CreateOrOpen("MySharedMemory", 4096);
// 创建视图(可读写)
using var accessor = mmf.CreateViewAccessor();
// 向共享内存写入整数
accessor.Write(0, 123456);
// 读取数据
int value = accessor.ReadInt32(0);
Console.WriteLine("读取的值: " + value); // 输出:123456
}
}
🎯 应用场景
- 大文件映射进内存,避免整文件加载
- 与其他进程共享内存(通过相同的
"MySharedMemory"名字) - 替代传统 TCP/管道 方式通信
✅ 方式二:使用 Marshal.AllocHGlobal 申请页大小的非托管内存
这适合用于与 C/C++ 库交互、需要精确控制内存时。
👇 示例:分配并访问一页大小的内存
using System;
using System.Runtime.InteropServices;
class Program
{
static void Main()
{
const int pageSize = 4096;
IntPtr ptr = Marshal.AllocHGlobal(pageSize);
try
{
// 写入一个整数
Marshal.WriteInt32(ptr, 0, 1000);
// 读取
int val = Marshal.ReadInt32(ptr, 0);
Console.WriteLine("读取的值:" + val);
}
finally
{
// 释放内存,防止内存泄漏
Marshal.FreeHGlobal(ptr);
}
}
}
🎯 应用场景
- 与底层 C++ 库共享内存
- 构建高性能缓存池
- 页对齐内存控制、SIMD 加速、数据包解析等
三、使用时需要注意什么?
| 注意点 | 原因 |
|---|---|
| 分配的大小应为页大小(4096 的整数倍) | 避免内存浪费,提高效率 |
| 使用完必须释放 | MemoryMappedFile 和 AllocHGlobal 都需要手动释放 |
| 不要让托管代码直接持有非托管指针 | 否则容易出现悬挂指针或内存泄漏 |
非托管内存操作需加 unsafe 或 Marshal 操作 |
以避免运行时错误 |
| 多进程访问要用相同名字映射文件 | 否则不能共享同一页 |
四、实际项目怎么使用?
假设你现在有个项目,需要多个组件共享数据,甚至多个进程共享:
场景 A:主程序把数据写到共享内存,子程序读取处理
// 写入方(主程序)
var mmf = MemoryMappedFile.CreateOrOpen("MyData", 4096);
var accessor = mmf.CreateViewAccessor();
accessor.Write(0, 9999);
// 读取方(另一个进程)
var mmf = MemoryMappedFile.OpenExisting("MyData");
var accessor = mmf.CreateViewAccessor();
int val = accessor.ReadInt32(0);
五、我该怎么学会它?
学习路线(建议你按顺序完成):
- ✅ 理解页的大小和作用:每页 4096 字节,不可拆分
- ✅ 掌握
MemoryMappedFile的用法(练习跨方法读写) - ✅ 掌握
Marshal.AllocHGlobal的分配与释放 - ⏳ 了解进阶内容(页对齐、虚拟内存布局、跨平台差异)
- ⛳ 在你当前项目中:找到适合分页内存的模块(如大数据缓存、实时共享等)
ReadOnlySpan memeory span、内存页、堆栈是什么关系
ReadOnlySpan<T>/Span<T>/Memory<T>- 内存页(Memory Page)
- 堆(Heap)、栈(Stack)
它们都和内存有关,但属于不同层次的抽象或用途。我们分层次梳理,先建立概念,再讲它们之间的关系。
🧱 一、三个核心概念介绍
✅ Span<T> / ReadOnlySpan<T> / Memory<T>
这些是 .NET 提供的高性能内存访问结构:
| 类型 | 所在内存 | 特点 |
|---|---|---|
Span<T> |
栈上或托管内存 | 用于短期、快速的数据操作,不能被 async 方法捕获 |
ReadOnlySpan<T> |
栈上或托管内存 | 只读版本,适合只读视图 |
Memory<T> |
堆上 | 可以跨 async 方法使用,可以转成 Span<T> |
它们本质上是:
“对一块已存在内存的轻量引用视图,而不是实际拥有这块内存”。
✅ 内存页(Memory Page)
这是操作系统层面的内存管理单位,比如 Windows 和 Linux 都以 4KB 为默认一页。
内存页定义了:
- 程序的虚拟地址空间如何分布
- 哪部分在物理内存、哪部分在磁盘(例如交换区)
- 页对齐(对性能有帮助)
内存页和
Span<T>/Memory<T>之间没有直接联系,但当你访问大量数据时,它们的访问会跨越多个内存页,会影响性能(如 TLB 缓存失效)。
✅ 堆 vs 栈(托管代码视角)
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配 | 快速、由编译器自动管理 | 较慢、由 GC 管理 |
| 生命周期 | 方法调用期间 | 手动/GC释放 |
| 示例 | 局部变量、Span<T> |
引用类型、Memory<T>、数组等 |
🧠 二、它们之间的关系(图 + 解说)
我们可以把它们看作是不同的“层”:
【硬件层】
└── 物理内存
└── 被划分为内存页(Page,通常为 4KB)
【操作系统层】
└── 虚拟内存(由内存页映射到物理内存)
├── 栈内存区域(函数局部变量、Span)
└── 堆内存区域(引用类型、Memory)
【.NET 应用层】
└── 变量引用内存页上的数据:
├── Span<T> / ReadOnlySpan<T>(轻量视图,通常位于栈)
└── Memory<T>(跨方法,通常引用堆上对象)
关系总结:
Span<T>/ReadOnlySpan<T>是对已有内存(栈上或托管堆上)的一种零分配视图Memory<T>是对堆上内存的引用,可以长时间持有和异步操作- 这些结构最终访问的实际数据,都可能跨越多个内存页
- 是否跨页影响性能(缓存失效、TLB miss)
📌 举个真实例子说明它们关系
byte[] buffer = new byte[8192]; // 在堆上分配了 2 个页大小(2x4096)
Span<byte> span = buffer.AsSpan(0, 100); // 创建 span 引用 buffer 的前 100 字节
这段代码:
- 堆上分配 8192 字节 => 操作系统分配了 2 页
Span<byte>只是对前 100 字节的视图(没有新分配内存)- 这 100 字节可能全在同一页内,也可能跨页
如果你处理大量数据、跨页读取较多,可能会频繁触发内存页加载、TLB miss,性能下降。
🎯 使用建议和注意事项
| 场景 | 推荐结构 | 原因 |
|---|---|---|
| 仅在方法内部处理小块数据 | Span<T> / ReadOnlySpan<T> |
无GC、速度快 |
| 跨方法/异步处理 | Memory<T> |
生命周期长、可用作缓存池 |
| 与非托管内存交互 | Span<byte> + Marshal / UnmanagedMemoryAccessor |
精细控制 |
| 需要页对齐(性能优化) | 非托管内存 + 页大小对齐 | 避免跨页,提高效率 |
✅ 总结一句话:
Span<T>/Memory<T>是对内存中一块数据的“高效访问视图”,而内存页决定了这些数据底层是如何被操作系统组织和访问的,最终落在堆或栈。
如果你项目中涉及:
- 使用大内存块处理高频数据
- 想避免不必要的内存分配
- 与非托管代码(如设备驱动、共享内存)打交道
那你应该合理结合使用:Memory/Page/Span/Stack/Heap 这些概念。

浙公网安备 33010602011771号