.Net Framework内存管理简介

一直以来我很少讨论这样的主题,今天我也不打算长篇累牍的介绍这个细节,但是我还是认为其中有一些重要的细节是值得每个程序员看一下的。

 

为啥要看一下?

作者和他的同时经常被问及关于GC如何工作的问题。但是,这些内部细节对程序员而言并不是必知的。一个程序员不应该关心FrameWork背后的细节,啥啥啥的。。。这不是自寻烦恼么?

如果你是个公交车司机,你绝对不用知道怎样去制造它的引擎。会踩刹车和油门的基本动作基本上就够了。但是如果你是个F1赛车手,那你就会想对你的赛车有足够的了解。所以你得首先明白你的工作需要,如果你仅仅是对web程序或者WinForm做些小的修改和添加,那你就不需要知道的太多(知道太多的结果,你知道的)。如果你想做些更底层的工作,那我倒是建议你可以看看这篇文章。

 

托管堆的纠结的前世今生

为了效率,托管堆会试着去找出一些对象设置为“重要的”,这些“重要的”被定义为不需要经常去检查的对象,就像被存放在资料室里的老档案。因为有些对象很有可能伴随着整个程序的运行周期而存在,而有些用完了即可以释放。所以一个对象存在的时间越长,那GC检查它的周期将会越长。这个算法的实现思想是将托管堆分为三代,即零,一,二代。一个对象创建的时候会被放入0代内存,随着生存周期的越来越长,它将被逐渐从0代移动到2代。举个这样的例子:

  • 经理要求你打电话给一个顾客。你对数字的记忆力超群,所以你就一直把电话号码记在脑子里(0代内存)。
  • 第一次打过去,没人接电话。所以你得过一会儿再试着打过去。这是经理又给你另外一个顾客的电话。
  • 你的工作经验告诉你能够记住的电话号码个数最多为10个,所以一旦经理给了你一个新的电话号码,而你记忆中的号码个数刚好到10个的时候,你就得把一个电话写在纸上(1代内存)。
  • 一天下来不断的有电话号码被写到纸上。
  • 你把电话写满了整整一张纸,纸上的号码已经凌乱不堪。所以你得把这些号码分下类,看看哪些是还可以再尝试几次的。你必须抛弃一些号码,并将剩下的保留到你的号码本上(2代内存)。

前面说了,当一个对象被创建的时候就被放入了0代内存。0代对象的初始容量决定于处理器的高速缓存大小。而后这个大小根据程序创建对象的频率会动态更改。一旦0代内存达到了容量的上限,GC首先检查0代中的每一个对象,并给一些不再使用的对象贴上“垃圾”的标签,并将它们移出0代。这是个清扫阶段。这次清扫中存留下来的对象将被移入1代内存。和0代一样,1代的容量也是根据程序创建对象的频率动态更改的。而一旦1代容量达到上限,GC又会清扫并且移动剩下的对象到2代,然后腾出的空位再清扫并移动0代内存。

一种良好的GC分配比例是100(0代):10(1代):1(2代),正常情况下0代做一次垃圾回收话费的时间是毫秒级的。1代的GC代价为30毫秒左右。2代的GC只在程序空闲的时候执行。

 

大对象托管堆

GC中一个很重要的概念叫“大对象托管堆”(LOH)。这里被存放所有大于85Kb的对象。LOH在每次“需要申请新的内存段的时候”(下一段会介绍)被回收。LOH和0,1,2代内存回收机制不同,LOH回收行为会触发0,1,2代内存的回收。

问题是我经常创建一些十分复杂的类,这些类看上去应该都超过了85Kb的大小了,它们都是LOH么?不一定。举例来说一个数据集(Dataset)一般包含了非常多的数据,但是数据集对象在内存中都仅仅只是保存数据的引用。你认为一个指针的数组有可能超过85Kb么?

 

内存段

托管堆一般被分成几个段。每一段的大小根据系统配置文件来决定。如果你的内存段大小将会达到64MB,否则的话是32MB。而LOH所在内存段为16M。只有2代内存和LOH会占用几个段的内存。

 

GC是怎么工作的?

以下是GC的工作流程:

  • GC检查LOH上每个对象的引用计数。如果为0,这个对象将被标记。
  • 所有LOH上被标记的对象都被释放。
  • LOH不会做内存碎片整理。
  • 扫描并标记2代内存上的对象。
  • 释放2代被标记的对象。
  • 对2代内存做碎片整理。
  • 扫描并标记1代内存上的对象。
  • 释放1代被标记的对象
  • 对1代内存做碎片扫描。
  • 更改2代内存的结束地址,将1代留下的对象放入2代中。
  • 扫描并标记0代内存。
  • 释放0代内存中被标记的对象。
  • 对0代内存做碎片整理。
  • 更改1代内存结束地址,将0代留下的对象放入1代中。

一些小的建议

这个主题可以很大,但是这里说些比较重要的

 

尽量不要把对象塞进1代内存

虽然1代内存看上去比2代内存性能要好,但是请记住2代内存设计目的是,仅仅存放那些生存周期贯穿整个程序运行时的对象。而0代内存的作用就是存放那些中间过程产生的对象。换句话说程序设计必须中间过程的对象生存周期尽量短,以至于除了必要的2代对象之外,其他的对象能够在0代内存执行GC前尽量释放。

 

不要调用GC.Collect()

这点重申一百遍都不为过!你过去,现在以及将来都不应该调用到GC.Collect()方法。只有一个情况下你可以使用它:测试!GC被设计成为自组织的智能机器。如果你试着去破坏它的平衡性,你就该想想那些腐败的政府,当众抠鼻孔。。。任何你想得到的恶心的场景。

 

避免大对象

如果你的程序不需要超过85kb的类,那就请尽量不要创建出大于85kb的类。如果真的需要,你应该认真的考虑如何提高这些对象的再利用率。

 

不要使用终结器(finalizers)

当你的对象有终结器的话它的终结函数(finalize)应该被手动调用,之后对象就会消亡。看起来这很自然而优雅。但是很不幸的是真实情况是你的对象只会被移动到下一代内存当中,因为他并没有准备好被回收。这意味着所有实现了终结器的对象真正消亡的时间最早也是在1代内存中了,并且很有可能是在2代内存。

 

关于GC,其实还有很多没说明白。不过你可以去别的博客上转转,看看其他N人是怎么说的。

本文完。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值