字符串如何在堆中分配内存?

时间:2015-04-23 03:16:46

标签: c# vb.net heap-memory dynamic-memory-allocation

在创建String类的对象时,我对内存分配感到困惑。我创建了一个示例应用程序,演示了在声明字符串对象时分配了多少内存。然后我尝试增加字符串的长度,以查看堆中消耗的总内存差异。

我的测试代码在这里

static void Main(string[] args)
{
    long l1 = GC.GetTotalMemory(false);
    long l2 = 0;

    Console.WriteLine(l1.ToString());

    myFunc();

    l2 = GC.GetTotalMemory(false);
    Console.WriteLine(l2.ToString());
    Console.WriteLine(String.Format("Difference : {0}", (l2-l1)));
    Console.ReadKey();
}

private static void myFunc()
{
    String str = new String('a', 1);
}

当我执行此代码时输出:

775596 //Memory at startup
816556 //After executing function
Difference : 40960

以上输出对于字符串长度0到2727是相同的。例如,即使我创建长度为2727的字符串对象,out也与上面相同。

String str = new String('a', 2727);

但是,当我在值中增加一个并为2728创建一个字符串时,输出会有所不同。

775596 //Memory at startup
822780 //After executing function
Difference : 47184

我也在VB.Net控制台应用程序中尝试过它。在VB.Net输出中,0到797长度的字符串相同。但是,当我将值增加到798时它会改变。

我不知道它是如何根据字符串长度分配内存的?

字符数组(字符串)表示它有2727项97字节(对于字符' a')。我认为它将值与字符字节相乘。我知道字符类型的固定长度为256字节。但是,我只是想知道它为什么会发生?所以,我也尝试改变角色来自' a'到了'。但是,结果与预期相同。

任何人都可以清楚地描述在声明任何字符串或其他类对象时如何分配内存?

2 个答案:

答案 0 :(得分:1)

来自the documentation

  

检索当前认为要分配的字节数

换句话说,此方法返回的值是而不是对实际分配的所有字节的精确计算。

我不知道该方法的确切实现,但我发现有一些低优先级进程正在监视堆中的高水位标记并不会让我感到惊讶为了提供有问题的价值。 (顺便说一句,对我而言,你的第一个差异可以达到2 ^ 12 * 10,这很有点巧合。)

请注意,返回值的这种不精确性实际上并没有告诉你任何关于"如何分配内存的信息"。我不确定你的问题是否真的只是"为什么这个价值不会像我预期的那样改变,或者如果你正在寻找更详细的解释一般情况下如何在.NET中分配对象。

但如果你想了解后者的更多信息,实际上有一些非常好的文章,包括Jeffrey Richter在MSDN上的这一对:

它们有点陈旧,并没有涵盖GC中的一些新功能,但基本功能并没有真正改变AFAIK,这些文章是恕我直言。

简短版本是,对于string类型,因为它是不可变的,字符串的缓冲区可以根据字符串的长度直接分配(请注意,这与类似{的类不同{1}}或List<T>,它们具有更复杂的数据结构,因此以更复杂的方式运行.NET内存管理器。)

由于.NET内存管理器的工作方式,对象的新分配只是查看指向堆分配部分当前末端的指针,并将其用于新对象,以及将指针移动到你已分配的字节数。

StringBuilder类型在.NET中是一种非常特殊的类型,因为它获得了本机代码支持和内部缓冲区的特殊处理,但是在堆上分配的基本思想仍然适用。) p>

同样,这些都无法解释您所看到的行为。但它是对内存分配如何发生的更广泛问题的回答。


回到string方法的问题,我确实从现已解散的关于.NET的新闻组中找到了这个有趣的讨论,在PC Review的网站上存档(可能在其他地方,但这是我发现的地方)它): What does GC.GetTotalMemory really tell us?。讨论蜿蜒一点,我不认为它真正解决了你正在问的问题。但无论如何,你可能会发现这是一个有趣的阅读。

答案 1 :(得分:1)

The only problem I see is with your method of research.

int[] lengths = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 64, 128, 256, 512, 1024, 2048, 4096 };
string[] strs = new string[lengths.Length];
long[] deltaMemory = new long[lengths.Length];

// We preload the functions we will use
var str0 = new string('A', 1);
var length0 = str0.Length;
long totalMemory0 = GC.GetTotalMemory(true);
long lastTotalMemory = totalMemory0;

for (int i = 0; i < lengths.Length; i++)
{
    strs[i] = new string((char)('A' + i), lengths[i]);
    long totalMemory = GC.GetTotalMemory(true);
    deltaMemory[i] = totalMemory - lastTotalMemory - lengths[i] * 2;
    lastTotalMemory = totalMemory;
}

Console.WriteLine("IntPtr.Size: {0}", IntPtr.Size);
for (int i = 0; i < lengths.Length; i++)
{
    Console.WriteLine("For size: {0}, extra memory: {1}", strs[i].Length, deltaMemory[i]);
}

You have to remember various things:

  • Don't allocate memory in any way other than the one you are measuring

  • Remember that the first time a method is called it must be JITted. I'll say that this operation eats memory. Pre-call once all the methods you'll use

  • A String in .NET is UTF-16, so each character 2 two bytes (lengthts[i] * 2)

  • There is surely some rounding around because memory is allocated in fixed chunks, of size connected to the size of IntPtr (so depending if you are working at 32 or 64 bits)

The result:

IntPtr.Size: 8
For size: 1, extra memory: 30
For size: 2, extra memory: 28
For size: 3, extra memory: 26
For size: 4, extra memory: 32
For size: 5, extra memory: 30
For size: 6, extra memory: 28
For size: 7, extra memory: 26
For size: 8, extra memory: 32
For size: 9, extra memory: 30
For size: 10, extra memory: 28
For size: 11, extra memory: 26
For size: 12, extra memory: 32
For size: 13, extra memory: 30
For size: 14, extra memory: 28
For size: 15, extra memory: 26
For size: 16, extra memory: 32
For size: 17, extra memory: 30
For size: 18, extra memory: 28
For size: 19, extra memory: 26
For size: 20, extra memory: 32
For size: 21, extra memory: 30
For size: 22, extra memory: 28
For size: 23, extra memory: 26
For size: 24, extra memory: 32
For size: 25, extra memory: 30
For size: 26, extra memory: 28
For size: 27, extra memory: 26
For size: 28, extra memory: 32
For size: 29, extra memory: 30
For size: 30, extra memory: 28
For size: 31, extra memory: 26
For size: 32, extra memory: 32
For size: 64, extra memory: 32
For size: 128, extra memory: 32
For size: 256, extra memory: 32
For size: 512, extra memory: 32
For size: 1024, extra memory: 32
For size: 2048, extra memory: 32
For size: 4096, extra memory: 32

So each string has (at 64 bits) an extra 26-32 bytes allocated. Mmmh... I see that Skeet even wrote a blog post on memory allocation: http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/