The Stack Is An Implementation Detail, Part Two

来源:互联网 发布:宇喜多秀家数据 编辑:程序博客网 时间:2024/06/16 04:26

栈是一个实现细节(二)
原文
Part one

很多人问过我,在之前的博文中提到过值类型会是在栈上,为什么值类型可以分配在栈上而引用类型不行。

简短的回答是“因为他们能”。因为栈结构的性能代价小,所以如果可能的话,我们把值类型放到栈上。

详见下文。
我将对内存管理策略做一次高级别的说明,我们一般称之为栈和堆。先从堆开始

公共运行语言的垃圾回收堆是充满了大量细节的编程奇迹。接下来的草图不是它如何运行而是十分近似的描述这个想法。
其思想是引用类型的实例保留大量的内存块。这内存块可以有“孔”,代表跟一些正在运行的对象相关的内存,一些是可以被新创建的对象使用的空闲内存。
理想的情况是,所有已使用的内存捆在一起,同时一大块未使用的内存在顶端。

如果在这种情况下,当新的内存被分配然后高水位标记就会上升,吃掉之前空闲的内存块。新的保留内存对于刚分配的过来的引用类型实例是可用的。
这是相当廉价的。只要简单的指针移动,如果有必要的话,加上新保留的内存置零。
如果我们有内存孔的话,我们可以维护一个空闲列表 - 一个空闲内存块链表。然后我们可以搜索链表寻找跟已用的内存孔相似的的空闲内存块并填满它。
因为需要进行链表搜索所以性能损耗有点昂贵。如果可能,我们尽量避免这种不理想的情况。

当执行垃圾回收时,这里分为三个阶段: 标记,清扫和压缩。
在标记阶段,我们假设堆中所有的内容是死的,CLR知道当回收时必须保证存活的对象,并标记这些必须保证运行的对象以及他们所引用的对象等等,
直到需要存活对象的传递闭包都被标记,进行下一阶段。
在清扫阶段,所有标记为死掉的对象都被转变成孔。
在压缩阶段,内存块被重新组织以便他是一个连续的内存,没有已用的内存孔。

这个草图实际上更复杂,CLR是分代的,会有3次这样的回收活动。
对象一开始在短存堆,如果他们一直存活就被移动到中间存活堆,如果他们一直存活在内存中,最终会被移动到长期存活堆。
GC在短存堆中运行频率最高,在长期存活堆中最低。这个想法是我们不想不断地花费成本去检查长期存活对象是否仍旧运行。
同时,我们也希望短期存在的对象能够被迅速的再使用。

GC有大量经过仔细调整的策略来确保高性能;他努力平衡内存和筛子一样堆的时间成本以防止压缩阶段的高成本。
非常大的对象存储在一个有不同的压缩策略的特别的堆等等。
我不知道所有的细节,并且很幸运,我不需要。(当然,我还遗漏了许多与本文没有关系的额外复杂性,即固定、终结和弱引用等等)

现在类比栈和堆。栈和堆一样是一大块有着高水位标记的内存。区别是栈地步的内存总是存活时间总是比顶端的长;栈进行严格排序。
在顶端的对象总是先辈标记死掉,底部的对象总是最后死掉。有了这个保证,我们可以知道栈永远不会有孔,因此也就不需要压缩阶段。
栈内存总是先释放顶部因此也就不需要记录空闲内存的列表。栈的内部的对象总是会保证存活所以也就不需要标记和清扫阶段。

对栈结构而言,内存分配只是一个单重指针的移动-与堆内存最好的情况一致。但是因为之前的保证措施,栈的回收也是一个单重指针的移动。
在这里节约了巨大的时间成本。许多人认为堆内存分配是昂贵的,栈内存分配是廉价的,其实都一样。
是回收的成本- 不同的存活时间迭代中德标记,清扫和压缩机内存的移动- 这时堆比栈花费海量的成本。

显然如果可以的话,用栈必用GC堆好。什么时候用呢?只有当你能保证栈工作的所有必要条件都满足时可以用栈。局部变量和值类型的形参是最好的实现方法。
在栈底部的局部框架存活的时间比顶部的长。局部值类型按照值来复制,不是引用,所以局部变量是唯一指向内存的;也就没有必要去追踪谁引用一个特定值来决定他的生命周期。
将引用传到局部的唯一方式是使用ref/out标识符,而这只是将引用传到栈结构的上部。无论如何局部变量将继续存活,所以事实上在调用栈结构时其上方有一个引用类型并不会改变他的生命周期。

这里继续。局部变量的值类型可以分配到栈上的原因。
1,正常的局部变量有严格的生存周期,2,值类型总是按值复制,3,将引用类型存储在局部的任何可能比局部变量存活周期长的地方都是非法的。
与之相对的,引用类型的生命周期依赖于存活的引用类型的数量,按引用地址复制及引用可以被存储在任何地方。
引用类型给您带来额外灵活性的代价是更复杂,更昂贵的垃圾回收策略

重申,所有的这些都只是实现细节。局部值类型使用栈只是CLR代替您实行的优化措施。值类型最相关的特点是语义上他们是按值进行复制,而不是有时他们可以在运行时的回收阶段进行优化

编注
{
主要解释为什么不能使用“ref int”字段。如果你这样使用了你也可以存一个指向短存活的局部变量到一个长期存活的对象中。
假设那是合法的,然后使用栈作为内存管理技术将不再有可见优化;值类型数据只是另一种引用类型将需要被垃圾回收。

匿名函数闭包和迭代块闭包都是后台通过建局部变量和形式参数转化为字段。这就是为什么在匿名方法和迭代块中捕捉ref/out形式参数是不合法的。
那样做并不是合法的字段。

其实,我们不希望在语言中有丑陋和奇怪的规则就像“您可以关闭任何本地或值参数,而不是引用或out参数”。
但是因为我们想要能够在堆栈中进行值类型的优化,所以我们选择将这个奇怪的限制加入到语言中。设计一如既往,是寻求妥协的艺术。

最后,CLR允许引用返回类型“ref return types”; 理论上,您可以使用一个方法 “ref int M() { … }” 返回一个整数变量的引用。
即使她出于某种异乎寻常的原因我们决定在c#中允许,我们必须解决编译器和校验,确保只有可能参返回变量被认为是在堆上,或已知比被降低到栈上。
}

阅读全文
0 0
原创粉丝点击