.NET内存问题:不受控制的LOH大小及可能的解决方案
在这篇文章中,我将尝试解释一个众所周知的内存大小不受控制的问题,并展示我的解决方案。
引言
.NET中的托管内存分为“栈”内存和“堆”内存。“堆”内存最重要的类型是SOH(小型对象堆)和LOH(大型对象堆)。从内存类型的名称可以看出,SOH仅存储小型对象,而LOH存储大型对象。在这篇文章中,我将考虑LOH内存可能出现的问题。其他内存类型不在本文讨论范围内。
LOH
CLR如何决定哪些对象必须存储在LOH中,哪些对象必须存储在SOH中?不幸的是,我找不到这个问题的确切答案,但我自己的实验和调查表明,当对象大小超过80000字节时,就会在LOH中创建对象。可能微软选择使用这个内存大小是因为测试显示它在性能和内存使用之间取得了平衡,但这需要向微软的人们提问。内存的大小可以通过编程方式获得,或者可以使用一些工具,例如“Process Explorer”。
LOH的一些特性及其相关问题
- LOH中的对象不能移动;这意味着内存会变得碎片化,内存调用会变慢。
- 即使GC完成了垃圾回收,LOH内存只会增加,而不会减少。这意味着你可能会遇到`OutOfMemoryException`异常。这个问题在内存有限的x86机器上尤其突出,每个进程只有3GB内存。
此类问题只有在使用大量结构体时才会出现。创建大量结构体不是一个好习惯,但是如果我们已经有数百万条记录的大型数组,并且所有这些记录都是值类型,我们该怎么办呢?首先,我们可以尝试将解决方案中的所有`struct
`关键字替换为`class
`。对于大多数类型的应用程序,这可以解决问题,但我们会遇到另一个问题:大量的`NullReferenceException`异常将困扰我们的整个解决方案。我的解决方案很简单:我们可以使用分页数组(数组的数组)代替简单的、一维数组。
LargeList实现
首先,我们应该计算对象大小,以决定是否应该为此对象扩展数组或使用一维数组。`sizeof`运算符只能在非托管上下文中使用,但是.NET Framework提供了一个`Marshal`静态类,其中包含`SizeOf`方法,这正是我们需要的。这是一个返回任何对象类型大小的方法。
public static int GetSize<T>()
{
Type type = typeof(T);
int result;
if (type.IsValueType)
{
result = type.IsGenericType ?
Marshal.SizeOf(default(T)) : Marshal.SizeOf(type);
}
else
{
result = IntPtr.Size;
}
return result;
}
现在只需要正确实现`IList<T>`接口及其方法和属性,并将集合存储在分页数组中(参见`LargeList`类中的`_items`字段)。
public class LargeList<T> : IList<T>
{
private const int DefaulPageSize = 80000;
private readonly int _objectSize;
private readonly int _itemsInPage;
private T[][] _items;
private int _count;
private int _pagesCount;
...
}
在这篇文章的下载文件中,您可以找到具有完整实现和单元测试的`LargeList`。我认为理解`LargeList`的代码并不难,特别是由于`LargeList`具有`IList`的标准方法和一些非常简单的有用方法。我已经进行了一些性能测试,并比较了`LargeList`和.NET `List`的性能。结果并不令人满意:来自`System.Collections.Generic`命名空间的标准.NET `List`的运行速度比`LargeList`快了好几倍。我认为这是因为`LargeList`没有`Capacity`属性,并且在每次Insert/Update/Delete操作后都会重新分配内存。您可以自己实现`LargeList`中的`Capacity`,或者等待我完成。我计划实现`Capacity`并进行一些性能测试;此外,我还计划在`LargeList`中实现某种排序并提高性能。
感谢您的阅读,希望这篇文章可以帮助某些人节省时间;但我的建议是:根本不要使用结构体。
历史
- 1.0 - 现在`LargeList`支持`Capacity`属性,并且不会在每次Insert/Delete操作后分配内存。