【问题标题】:How string allocates the memory in heap?字符串如何在堆中分配内存?
【发布时间】:2015-07-01 00:33:15
【问题描述】:

我在创建 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 时,它会发生变化。

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

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

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

【问题讨论】:

  • 您可以随时查看源代码。 referencesource.microsoft.com/#mscorlib/system/…
  • 另外,请记住,您实际上在内存检查之间分配了 2 个字符串,而不仅仅是一个。 Console.WriteLine(l1.ToString()); 也在分配一个字符串。
  • 记住,字符不是字节! 1 个字符 = 2 个字节

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


【解决方案1】:

来自the documentation

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

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

我不知道该方法的确切实现,但我不会惊讶地发现其中涉及一些低优先级进程,它正在监视堆中的高水位标记以提供有问题的价值。 (顺便说一句,有趣的是,你的第一个差异是 2^12 * 10)。

请注意,返回值中的这种不精确性实际上并没有告诉您“内存是如何分配的”的任何信息。我不确定您的问题是否真的只是“为什么这个值没有改变我期望的方式”,或者您是否正在寻找关于如何在 .NET 中分配对象的更详细说明。

但如果你想进一步了解后者,其实有一些相当不错的文章,包括 Jeffrey Richter 在 MSDN 上的这对:

它们有点旧,并且没有涵盖 GC 中的一些新功能,但基础知识并没有真正改变 AFAIK,而且这些文章恕我直言,经久不衰。

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

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

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

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


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

【讨论】:

    【解决方案2】:

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

    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 字节。嗯……我看到飞碟还写了一篇关于内存分配的博文:http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/

    【讨论】:

      猜你喜欢
      • 2019-05-20
      • 2012-07-10
      • 2011-03-22
      • 1970-01-01
      • 2023-03-25
      • 1970-01-01
      • 1970-01-01
      • 2021-07-27
      • 1970-01-01
      相关资源
      最近更新 更多