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

How string allocates the memory in heap?

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

我的测试代码在这里

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);

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

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

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

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

字符数组(字符串)表示它有 2727 个项目,共 97 个字节(对于字符 'a')。我认为它将值乘以字符字节。我知道字符类型的长度固定为 256 字节。但是,我只是想知道为什么会这样?所以,我也尝试过将字符从 'a' 更改为 'z'。但是,结果和预期的一样。

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

来自the documentation

Retrieves the number of bytes currently thought to be allocated

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

我不知道该方法的确切实现,但如果发现其中涉及一些低优先级进程正在按顺序监视​​堆中的高水位线,我不会感到惊讶提供有问题的价值。 (顺便说一句,对我来说有趣的巧合是你的第一个差值是 2^12 * 10)。

请注意,返回值的这种不精确性实际上并没有告诉您有关 "how the memory gets allocated" 的任何信息。我不太确定您的问题是否真的只是 "why doesn't this value change the way I expected it to",或者您是否正在寻找有关通常如何在 .NET 中分配对象的更详细解释。

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

它们有点老了,没有涵盖 GC 中的一些新功能,但基础知识并没有真正改变 AFAIK,恕我直言,这些文章是永恒的。

简短的版本是,对于 string 类型,因为它是不可变的,所以可以根据字符串的长度直接分配字符串的缓冲区(注意,这不同于例如 class 类似于 List<T>StringBuilder,它们具有更复杂的数据结构,因此最终以更复杂的方式使用 .NET 内存管理器)。

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

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

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


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

我看到的唯一问题是你的研究方法。

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 甚至写了一篇关于内存分配的博客 post:http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/