.NET 进程中的内存限制






4.90/5 (37投票s)
本文旨在介绍 .NET 内存管理以及运行时和平台为每个进程设定的内存限制。我们还将提供一些有关如何处理在达到这些限制时遇到的问题的技巧。
本文旨在介绍 .NET 内存管理以及运行时和平台为每个进程设定的内存限制。我们还将提供一些有关如何处理在达到这些限制时遇到的问题的技巧。
进程可用内存
如您所知,无论您在计算机上安装多少物理内存,您的应用程序都会遇到各种问题,这些问题会限制其可用的实际内存。
例如,32 位系统最多只能拥有 4 GB 的物理内存。不用说,2^32 将为您提供一个包含 4,294,967,296 个不同条目的虚拟地址空间,这正是 4 GB 限制的来源。但即使系统中有这 4 GB 可用,您的应用程序实际上只能看到 2 GB。为什么?
因为在 32 位系统上,Windows 将虚拟地址空间分成两部分:一部分用于用户模式应用程序,另一部分用于内核(系统应用程序)。可以通过在 Windows 的 boot.ini 配置文件中使用“/3gb”标志来覆盖此行为。如果这样做,系统将为用户应用程序预留 3 GB,为内核预留 1 GB。
但是,除非我们在应用程序映像头中明确激活另一个标志:IMAGE_FILE_LARGE_ADDRESS_AWARE,否则我们仍将只能从应用程序中看到 2 GB。在 32 位操作系统上组合使用这两个标志通常称为:4GT (4 GigaByte Tuning)。
令人惊讶的是,在64 位环境中,问题非常相似。尽管这些系统在物理内存或为内核保留的地址空间方面没有相同的限制(事实上,在这些系统上“/3gb”标志不适用),但进程在尝试寻址超过 2 GB 时会遇到同样的限制。除非为可执行文件设置了相同的标志(IMAGE_FILE_LARGE_ADDRESS_AWARE),否则默认限制将始终相同。
激活标志:IMAGE_FILE_LARGE_ADDRESS_AWARE
- 在原生 C++ 应用程序中,设置此标志非常简单,因为 Visual Studio 提供了该选项。您只需设置“/LARGEADDRESSAWARE”链接器参数即可。
- 在 C# .NET 应用程序中
- 编译为 64 位的应用程序默认会设置此标志,因此您将可以访问 8 TB 的地址空间(取决于操作系统版本)。
- 编译为 32 位的应用程序需要使用名为 EditBin.exe 的工具(随 Visual Studio 分发)进行修改。此工具将为您的 EXE 设置适当的标志,使您的应用程序在 64 位 Windows 上运行时能够访问 4 GB 的地址空间,或在启用了 4GT 调优的 32 位 Windows 上运行时能够访问 3 GB 的地址空间。
下表(摘自此处)总结了根据平台和我们运行的进程类型,虚拟地址空间的限制。
此页面提供了有关此问题的更多信息。
系统内存限制。比您预期的更近
如今,内存价格便宜。然而,如前一章所述,尽管您的 PC 安装了大量物理内存,但在许多情况下,您最终只能获得 2 GB 可用内存。
此外,如果您的应用程序是用 .NET 开发的,您会发现运行时本身会引入显著的内存开销(约 600-800 MB)。因此,当内存使用量达到 1.2 GB 或 1.3 GB 时开始收到 OutOfMemory 异常并不奇怪。此博客对此进行了进一步讨论。
因此,如果您不属于地址空间超出 2 GB 的情况,并且您正在使用 .NET 开发,那么您的实际内存限制将约为 1.3 GB。
这对于 99% 的应用程序来说已经足够了,但其他应用程序,如密集计算应用程序或与数据库相关的应用程序,可能需要更多。多得多……
情况变得更糟……
为了让事情变得更复杂,您很快就会了解到,拥有一笔可用内存与找到一块连续可用内存是两回事。
正如大家所知,由于操作系统内存管理,诸如分页以及对象创建和销毁等技术使得内存越来越碎片化。这意味着即使存在一定量的可用内存,它也分散在一堆小的空隙中,而不是有一块单一的大块内存可用。
现代操作系统和 .NET 平台本身采用方法来防止碎片化,例如所谓的压缩(移动内存中的对象以将几个空闲内存块合并成一个更大的块)。尽管这些技术减少了碎片化的影响,但并未完全消除它。本文详细介绍了 .NET垃圾回收器 (GC) 的内存管理以及它执行的压缩任务。
在此文章的上下文中,碎片化是一个大问题,因为如果您需要分配一个 10 MB 的连续数组,即使您的进程有 1 GB 的可用内存,如果系统找不到该大小的连续内存块,您也会收到OutOfMemory 异常。当处理大数组时,这种情况比您预期的更频繁地发生。
在 .NET 中,碎片化和对象的压缩与对象的大小密切相关,所以我们稍后再谈谈这个问题。
大对象分配
您可能不知道,直到最新版本(1.0、2.0、3.0、3.5 和 4.0)的所有 .NET 版本都对单个对象的最大大小有限制:2 GB。无论您是在 64 位还是 32 位进程中运行,都无法在单个对象中创建大于该大小的对象。仅从 4.5 版本开始,该限制才被取消(仅适用于 64 位进程)。但是,除了极少数例外,如果您需要创建如此大的对象,您很可能是在应用错误的设计模式。
在 .NET 世界中,GC 将对象分为两类:小对象和大对象。您期待更技术性的东西吗?是的,我也是……但就是这样。任何小于 85000 字节的对象都被认为是小对象,任何大于该大小的对象都被认为是大对象。当 CLR 加载时,为应用程序分配的堆被分成两部分:SOH(小对象堆)和 LOH(大对象堆)。每种类型的对象都存储在相应的堆中。
值得注意的是,大对象的压缩成本非常高,因此在当前版本的 .NET 中不直接进行(开发人员表示这种情况可能会在未来改变)。对大对象执行的唯一类似压缩的操作是,两个相邻的死对象会被合并成一块空闲内存,但目前没有任何大对象被移动以减少碎片化。
这篇精彩的文章提供了有关LOH 的更多信息。
C# 数组在达到内存限制时
简单数组(或一维数组)是 C# 中消耗内存的最常见方式之一。众所周知,CLR 始终将它们分配为单个连续内存块。换句话说,当我们实例化一个 byte[1024] 对象时,我们请求 1024 字节的连续内存,如果系统找不到大小等于该值的连续空闲内存块,您将收到OutOfMemory 异常。
在处理多维数组时,C# 提供了不同的方法。
交错数组,或数组的数组:[][]
声明为byte[][],这是实现多维数组的经典解决方案。事实上,它是 C++ 等语言中唯一原生支持的方法。
关于内存分配,它们表现为一个简单的元素数组(一个内存块),其中每个元素是另一个数组(另一个不同的内存块)。因此,像 byte[1024][1024] 这样的数组将涉及分配 1024 个大小为 1024 字节的内存块。
多维数组:[,]
C# 引入了一种新的数组类型:多维数组,声明为byte[,]。
尽管它们使用起来非常方便且易于实例化,但它们在内存分配方面的表现完全不同,因为它们在堆上作为单个内存块分配,占数组的总大小。在前面的例子中,像 byte[1024, 1024] 这样的数组将涉及分配一个 1 MB 的单一连续内存块。
在下一章中,我们将对这两种类型的数组进行快速比较。
比较:[,] vs [][]
二维数组 [,](分配为单个内存块)
优点
- 消耗的内存更少(无需存储所有 N 个内存块的引用)
- 分配速度更快(分配一个更大的、单一的内存块比分配 N 个较小的内存块更快)
- 更易于实例化(一行即可完成:new byte[128, 128])
- 有用的工具方法,如 GetLength()。更简洁易用的用法。
缺点
- 寻找一块连续的内存块可能会成为问题,特别是处理大数组时,或者当达到进程内存限制时。
- 访问数组中的元素比交错数组慢(见下文)。
交错数组 [][](分配为 N 个内存块)
优点
- 更容易为这类数组找到可用内存,因为由于碎片化,更有可能存在 N 个较小的可用内存块,而不是一个完整大小的连续内存块。
- 访问数组中的元素比二维数组快,主要是因为编译器对简单一维数组的处理进行了优化(毕竟,交错数组由多个一维数组组成)。
缺点
- 比二维数组消耗的内存稍多(需要存储对 N 个简单数组的引用)。
- 分配速度较慢,因为它需要分配 N 个元素而不是一个块。
- 实例化不方便,因为您需要遍历数组元素来实例化它们(有关技巧,请参见下文)。
- 不提供工具方法,并且可能稍微更难阅读和理解。
此博客也对它们进行了很好的比较。
结论
每个用户应根据其具体情况决定哪种类型的数组最适合。然而,一个通常需要大量内存,并且比舒适性、易用性或可读性更关心性能的开发人员,可能会决定使用交错数组 ([][])。
技巧:自动实例化二维交错数组的代码
实例化多维交错数组可能令人困扰且重复。此通用方法将为您完成工作。
public static T[][] AllocateArray2D<T>(int pWidth, int pHeight)
{
T[][] ret = new T[pWidth][];
for (int i = 0; i < pHeight; i++)
ret[i] = new T[pHeight];
return ret;
}
希望有所帮助!!