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

3

在创建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的字符串对象,输出结果与上述相同。

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'更改为'z'。但结果与预期相同。
有人能清楚地描述一下当声明任何字符串或其他类对象时内存如何分配吗?

1
你可以随时查看源代码。http://referencesource.microsoft.com/#mscorlib/system/string.cs,8281103e6f23cb5c - Bradley Uffner
1
另外,请记住,在内存检查之间,您实际上已经分配了2个字符串,而不仅仅是一个。Console.WriteLine(l1.ToString());也在分配一个字符串。 - Bradley Uffner
请记住,字符不等于字节!1个字符=2个字节。 - Gabe
2个回答

1
我看到的唯一问题就是你的研究方法。
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]);
}

你需要记住以下几点:

  • 不要以任何方式分配内存,除了你正在测量的方式

  • 记住第一次调用方法时必须进行JIT编译。我会说这个操作会消耗内存。在使用所有方法之前先预调用一次

  • .NET中的String是UTF-16,所以每个字符占用2个字节(lengthts[i] * 2

  • 肯定有一些四舍五入,因为内存是按固定块分配的,大小与IntPtr的大小相关(因此取决于您是否在32位或64位上工作)

结果:

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

所以每个字符串(64位)都分配了额外的26-32字节。嗯...我看到Skeet甚至写了一篇关于内存分配的博客文章:http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/

1

来自文档:

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

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

我不知道该方法的确切实现,但我不会感到惊讶,因为涉及了某些低优先级进程来监视堆中的高水位标记,以提供所需的值。(顺便说一下,有趣的巧合是,你的第一个差异结果为2^12 * 10)。

请注意,返回值的这种不精确性实际上并没有告诉您任何关于"内存如何分配"的信息。我不确定你的问题是否只是"为什么这个值没有按照我预期的那样改变",还是你正在寻找更详细的解释,介绍在.NET中如何分配对象。

但如果您想了解更多相关内容,实际上有一些非常好的文章,包括Jeffrey Richter在MSDN上发表的这篇文章:

它们有点过时,不涵盖GC中的一些新功能,但是基础知识没有真正改变,那些文章在我看来是永恒的。

简而言之,对于string类型,由于其是不可变的,因此可以根据字符串的长度直接分配字符串的缓冲区(请注意,这与例如List<T>StringBuilder等类不同,这些类具有更复杂的数据结构,因此以更复杂的方式使用.NET内存管理器)。

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

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

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


回到GC.GetTotalMemory()方法的问题,我在PC Review网站(可能还有其他地方,但这是我找到的地方)中找到了这篇有趣的关于.NET的讨论What does GC.GetTotalMemory really tell us?。讨论有点漫无目的,我不认为它真正直接回答了你所问的问题。但你可能会觉得它很有趣。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接