前言
近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点。
1,.托管代码和非托管代码的区别
2.托管堆是什么?
3.托管堆基础,托管堆分配资源
4.内存泄漏 内存溢出、内存损坏
5.C# new操作符分配资源
6.垃圾回收算法
7.代:提升性能
8.垃圾回收触发条件
9.大对象
10.使用需要特殊清理的类型
11.使用包装了本机资源的类型
一、托管代码和非托管代码的区别
托管代码:执行过程交由运行时管理的代码。IL(中间语言)有时也成为托管代码。CLR负责提取托管代码,利用JIT编译为机器代码,从而执行它。(CLR提供 自动内存管理、安全边界、类型安全)
非托管代码:在CLR外部,由操作系统直接执行的代码。(按自己的想法提供垃圾回收、类型检查、安全)例如:COM 组件、ActiveX 接口和 Win32 API 函数,数据库连接
二、托管堆是什么?.
托管堆:CLR要求所有对象都从托管堆中分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。
(CLR还要维护一个指针,我们称它作NextObjPtr。该指针指向下一个对象在堆中的分配位置)
三、托管堆基础,托管堆分配资源
在面向对象中,每个类型都代表可供程序提供的一种资源,要使用这些资源,必须为代表资源的这些类型分配内存
1.调用IL指令newobj,为代表资源的类型分配内存(一般使用C#new操作符来完成)
2.访问类型的成员来使用资源(有必要可以重复)
3.摧毁资源的状态以进行清理
4.释放内存。垃圾回收器独自负责这一步
四、内存泄漏 内存溢出、内存损坏
内存泄漏:申请了一块内存,但不需要时一直不去删除,导致这块内存一直被占用
内存溢出:程序在申请内存时,没有足够的内存空间供其使用,抛出OutOfMemoryException
内存损坏:访问被释放的内存
五、C#的new操作符导致CLR执行以下步骤
1.计算类型的字段(以及从基类型继承的字段)所需的字节数
2.加上对象开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。
32位应用程序,这两个字段各自需要32位,所以每个对象要增加8字节
64位应用程序,这两个字段各自需要64位,所以每个对象要增加16字节
3.CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的可用空间,就在NextObjPtr指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操作符返回对象引用。就在返回这个引用之前,NextObjPtr指针的值会加上对象占用的字节数来得到一个新值,及下个对象放入托管堆时的地址
六、垃圾回收算法
1.引用计数算法。
Microsoft自己的“组件对象模型”(Component Object Model,COM),每个对象都维护着一个内存字段来统计程序中多少“部分”正在使用对象,随着每一“部分”到达代码中某个不再需要对象的地方,就递减对象的计数字段。计数对象变为0,对象就可以从内存中删除了。
许多引用技术系统最大的问题是处理不好循环引用。这种引用会阻止两个对象的计数器达到0,所以两个对象永远不会删除
2.引用跟踪算法
所有引用类型的变量都成为根。
CLR开始GC时,首先暂停进程中的所有线程,这样可以防止CLR检查期间访问对象并更改其状态。然后进入GC的标记阶段,将所有对象同步块索引的一位设为0,这表明所有对象都应被删除。任何根如果引用了堆上的对象,CLR就会将该对象的同步块索引中的位设为1,一个对象被标记后,CLR就会检查那个对象的根,标记他们的引用对象。如果发现对象已经标记,就不重新检查对象的字段。这样就避免了循环引用而产生死循环。
已标记的对象不能被垃圾回收。CLR知道哪些对象可以幸存,哪些对象可以删除后,会进入GC的压缩阶段,压缩所有幸存下来的对象,使它们占用连续的内存空间。减少了应用程序的工作集,从而提升将来访问这些对象时的性能。解决了本机堆空间碎片化的问题。
但是引用幸存对象的根现在引用的还是对象最初在内存中的位置,被暂停的线程恢复执行时,将访问旧的内存位置,会造成内存损坏,所以GC进入压缩阶段后,CLR还要从每个根减去所有引用的对象在内存中偏移的字节数。
压缩好内存后,NextObjPtr将指向最后一个幸存对象之后的位置,CLR恢复应用程序的所有线程。
注:静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止
有趣的三张图示留给读者思考:
图示一:
图示二:
图示三:
七、代:提升性能
CLR的GC是基于代的垃圾回收器,它对代码做了如下假设:
对象越新,生存期越短。
对象越老,生存期越长。
回收堆的一部分,速度快于回收整个堆。
托管堆在初始化时不包含对象。添加到堆的对象被称为第0代对象。CLR初始化时为第0代对象选择一个预存容量。如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。在一次垃圾回收后存活的第0代对象会成为第1代对象(事实上CLR还必须为第1代选择预算),第0代对象就不包含任何对象了。周而复始(第1代预算内存不满时不会自动释放内存),直到某一次,第0代和第1代预算内存都满了,第1代可能已经有许多对象变得不可达,这次垃圾回收器会检查第1代和第0代的所有对象,两代都被垃圾回收。和之前一样,第0代幸存者被提升为第1代,第1代幸存者被提升为第2代,第0代空出来了。
托管堆只支持三代:第0代、第1代和第2代。CLR初始化时,会为每次带选择预算。CLR的垃圾回收器是自调节的,会根据应用程序的内存负载自动优化。
八、垃圾回收触发条件
1.代码显示调用System.GC的静态Collect方法
2.Windows报告低内存情况
CLR使用Win32函数CreateMemoryResourceNotification和QueryMemoryResourceNotification监视系统的总体内存使用情况
3.CLR正在卸载AppDomain
CLR认为一切都不是根,执行涵盖所有代的垃圾回收
4.CLR正在关闭
整个进程都要终止了,Windows将回收进程的全部内存
九、大对象
CLR将对象分为大对象和小对象。
85000字节或更大的对象是大对象。
1.大对象不是在小对象的地址空间分配,而是在进程地址空间的其它地方分配
2.目前版本的CLR不压缩大对象,因为在内存中移动他们的代价过高
3.大对象总是第2代,绝不可能是第0代或第1代
十、使用需要特殊清理的类型
有的类型除了内存还需要本机资源(文件、网络连接、套接字、互斥体),GC回收托管堆中使用的内存,会造成本机资源的泄漏。
CLR提供了成为终结的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。
终结基类System.Object定义了受保护的虚方法Finalize。垃圾回收器判定对象是垃圾后,会调用对象的Finalize方法(如果重写)
C#要求在类名前添加~符号来定义Finalize方法,如下图所示:
被视为垃圾的对象在垃圾回收完毕后才调用Finalize方法,这些对象的内存不是被马上回收的。
因为Finalize方法可能要执行访问字段的代码,可终结对象在回收时必须存活,造成被提升到另一代,使对象活的比正常时间长,增大了内存好用,所以应尽可能避免终结。
注:Finalize方法的执行时间是控制不了的。
十一、使用包装了本机资源的类型
如果想允许使用者控制类所包装的本机资源的生存期,就必须实现IDisposable接口。
如果类定义的一个字段的类型实现了dispose模式,那么类本身也实现了dispose模式。
”dispose一个对象”真正的意思是:清理或处置对象中包装的资源(比如它的字段引用的对象),然后等着在一次垃圾回收之后回收该对象占用的托管堆内存(此时才释放)
using语言编译器自动生成try和finally块,在finnally块中编译器生成代码将变量转型为一个IDisposable并调用dispose方法。所以using语句只能用于实现了IDisposable接口的类型。
一个清理资源的范例:
class People:IDisposable { //这是一个Finalize方法 ~People() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool disposed = false; public virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { // Free any other managed objects here. } // Free any unmanaged objects here. } disposed = true; } }
参考:http://www.cnblogs.com/luminji/archive/2011/03/29/1997812.html
天道酬勤,大道至简,坚持。