generics内存管理

关于如何管理强类型generics的内存,我有疑问

List ints1 = new List(); ints1.Add(1); ints1.Add(2); ints1.Add(3); int[] ints2 = new int[10](); ints2.Add(6); ints2.Add(7); ints2.Add(8); 

问题1:我假设ints1初始化为一个新的关键字( new List() )它成为一个引用类型。 值1,2,3在哪里存储在内存中(它们存储在堆栈中还是堆栈中)?

问题2:我知道Listint[]之间的事情, List可以在运行时将其大小扩展到任何大小,这在编译时在int[]中受到限制。 因此,如果值1,2,3存储在堆栈中,如果将新项添加到List说4,它将不会连续到前3右,那么ints1将如何知道内存位置4?

我假设ints1初始化为new关键字new List()它成为引用类型

这种推定是不正确的。 您也可以在值类型上使用“new”关键字!

 int x = new int(); 

使用“new”不会使任何引用类型。 您可以将“new”与引用类型或值类型一起使用。 “new”表示将分配存储并调用构造函数

在值类型上使用“new”的情况下,分配的存储是临时存储。 对该临时存储的引用将传递给构造函数,然后将现在初始化的结果复制到其最终目标(如果有)。 (“new”通常与赋值一起使用但不一定是。)

在引用类型的情况下,存储被分配两次:为实例分配长期存储,并为实例的长期存储分配短期存储 。 引用传递给构造函数,构造函数初始化长期存储。 然后将引用从短期存储复制到其最终目标(如果有)。

使List成为引用类型的原因是List被声明为类。

值1,2,3在哪里存储在内存中(它们存储在堆栈中还是堆栈中)?

我们一直在努力创建一个内存管理器,让您无需关心存储内容的位置。 值存储在短期内存池(实现为堆栈或寄存器)或长期内存池(实现为垃圾收集堆)中。 根据值的已知生命周期分配存储。 如果已知该值是短暂的,则其存储在短期池中分配。 如果知道该值是短暂的,那么它必须在长期池中分配。

名单所拥有的1,2,3可以永远存在; 我们不知道该列表是否会比当前的激活帧更长。 因此,存储1,2,3的存储器被分配在长期池中。

不要相信“值类型总是在堆栈上分配”的谎言。 显然这不可能是真的,因为包含数字的类或数组无法在当前堆栈帧中存活! 在池上分配值类型,这对于已知的生命周期是有意义的。

int[]不同, List可以在运行时将其大小缩放到任何大小

正确。 看看List如何做到这一点很有教育意义。 它只是分配一个大于它需要的T数组。 如果它发现它猜得太小,它会分配一个新的更大的数组,并将旧的数组内容复制到新的数组中。 List只是一堆数组副本的方便包装器!

如果值1,2,3存储在堆栈中,并且新项目4被添加到列表中,那么它将不会连续到前三个。

正确。 这就是为什么没有在堆栈上分配值1,2,3的存储的一个原因。 存储实际上是在堆上分配的数组。

那么列表如何知道第4项的内存位置?

该列表分配的数组太大 。 当您添加新项目时,它会将其粘贴到太大的数组中的未使用空间中。 当arrays用完房间时,它会分配一个新arrays。

“new”语法用于初始化值类型和引用类型。 新列表在堆上创建; 这些值被加载到堆栈中(即在它们被添加到列表之前),但是一旦添加,它们就在堆上,在int[]中支持列表。 数组总是在堆上。

将它们复制到arrays的事实也回答了我相信的第2部分。 该数组过大,仅在满时重新分配。

注意; List不会“成为”引用类型; 它始终是参考类型。

generics的内存管理(通用集合)与非generics类型的内存管理完全相同。

你的ints1列表使用了一个数组。 所以它与ints2相同(当它被纠正时)。 在这两种情况下,Heap上的内存块都包含int值。

List<>类由一个数组,一个int Count和一个int Capacity属性组成。 当Add()元素Count递增时,当它传递Capacity时,将分配一个新数组并复制内容。

问题1: http : //msdn.microsoft.com/en-us/library/6sh2ey19.aspx说:

List类是ArrayList类的通用等价物。 它使用一个数组实现IList通用接口,该数组的大小根据需要动态增加。

这看起来像一个简单的数组,只要它溢出就重新分配。 AFAIKR在每次重新分配时大小加倍 – 我研究了一次,但不记得是什么。

该数组在托管堆上分配,就像您刚刚声明它一样。

  1. 无论你怎么看,List都是一个引用类型。 所有这些类型都在堆上分配。 我不知道C#编译器是否足够聪明,可以弄清楚在方法之外没有使用的对象可以在堆栈上分配(Eric Lippert可能会告诉我们),但即使它确实如此,这是你作为程序员不需要担心的事情。 它只是编译器将为您做的优化,而您从未注意到。

  2. int数组也是一个引用类型,它也在堆上分配,就像那样简单。 对于堆栈中某些假设的数组碎片没有任何意义,因为它们根本没有在堆栈中分配。

 List ints1 = new List(); 

我认为ints1初始化为一个新的关键字(new List())它成为一个引用类型。

我们首先要清楚一些术语:

  • ints1不是任何类型的类型,而是变量 ,因此它不能“成为引用类型”。
  • 变量具有静态(“编译时”)类型。 例如, ints1被声明为List类型。 变量的静态类型永远不会改变。
  • 变量引用的值或对象的类型称为动态(“运行时”)类型。 在上面的赋值之后, ints1的动态类型是List 。 每当为其分配新值或对象时,变量的动态类型可能会发生变化。

ints1的情况下,两种类型恰好相同,但并非总是如此:

 ICollection ints3 = new List(); 

这里,静态类型是ICollection (总是),动态类型是List (在赋值之后)。


对问题1的回答:

 ints1.Add(1); ints1.Add(2); ints1.Add(3); 

值1,2,3在哪里存储在内存中(它们存储在堆栈中还是堆栈中)?

官方的答案是,这应该被视为List的实现细节,对您来说无关紧要。 您需要了解的是您可以对List执行的操作(例如AddClear等)及其特性(例如前/后条件,性能等)。

如果您仍想知道此类型在内部如何工作,那么让我们从这个提示开始:

List类[…]使用一个数组实现IListgenerics接口,该数组的大小根据需要动态增加。List MSDN参考页面,备注部分

意思是,您可以将List想象成T[]类型的数组,在需要时增加其容量。 在ints1的情况下,想一个int[] 。 因为int是值类型,所以列表(1,2,3)中的具体值将存储在原位。 如果它是引用类型,则每个值将存储在单独的位置,并且该数组将仅包含引用。

请注意,我没有在上一段中提到术语“堆栈”,也没有提到“堆”。 这是因为这些概念是另一个实现细节,即.NET平台之一,你不应该太在意。 (参见Eric Lippert的博客文章系列,“堆栈是一个实现细节” ,例如我对前一个问题的答案,“新的总是在C ++ / C#/ Java中分配在堆上吗?” )


对问题2的回答:

List可以在运行时将其大小扩展为任何大小,这在编译时受int []限制。 因此,如果值1,2,3存储在堆栈中,如果将新项添加到List说4,它将不会连续到前3右,那么ints1将如何知道4的内存位置?

同样,你不需要考虑这一点。 List处在于它所提供的特定特性和操作是否适合您的目的。 但如果我把它留在那里,我真的不会回答你的问题,所以让我们简单地想一想:

当你谈到“堆栈”时,你可能意味着执行线程的调用堆栈 。 AFAIK,利用这种数据结构的虚拟机和编程语言主要用于将参数传递给函数/方法,也可能用于存储保持函数本地的值。 这与调用堆栈也用于局部变量不同,因为局部变量可能包含一个返回调用函数的对象,可以通过返回值,也可以通过ref / out参数等。

对我来说,似乎不太可能在正常情况下List对象最终会出现在调用堆栈上(我不考虑stackalloc或其他任何与unsafe上下文有关的事情。)。 请记住, List是一种引用类型:尽管可能,也许很可能,对实际对象的引用最终会出现在调用堆栈上,但很可能对象的数据本身不会。

IList一个(可能是天真的)实现使用固定大小的数组进行项目存储,面对增加容量的需要,可以动态分配一个新的,更大的数组,然后将当前数组的内容复制到新数组,然后放弃旧数组,转而使用新数组。 这样,所有物品都集中在一个地方。

问题1

C#中的列表内部包含数组。 List指的是堆上的位置,并且该位置是存储所有值的数组。 所以这些值存储在堆上。 如果数组是类的一部分,则数组也是如此。

问题2

堆栈是连续的,因此在堆栈上推送另一个int将意味着它的内存地址是前一个int + 4的位置。列表在添加项目时的工作方式是它们创建的数组大于您需要的数组。 当达到数组的长度时,会有一个算法创建一个更大的数组并复制当前值。

您可能感兴趣的另一件事是链接列表。 链接列表不能在内部使用数组,而是与节点一起使用。 每个节点包含数据和列表中下一个节点的位置。 双向链表包含具有所有该节点的节点以及列表中前一节点的位置。