.net 垃圾回收学习【.net 框架程序设计】[CH19: 自动内存管理]
2011-08-24 20:25 一一九九 阅读(269) 评论(0) 编辑 收藏 举报19.1 基本原理解析
(一)访问一个资源所需要的几个步骤:
- 为类型实例分配一定的空间
- 初始化内存, 设置资源的初始化状态
- 访问类型成员来使用资源。
- 销毁资源状态,执行清理工作。
- 释放内存。
由于垃圾收集器对内存中的类型表示何种资源一无所知,所以垃圾收集器对第四步并不清楚如何执行,为了使资源得到正确的清理,开发人员必须自己编写执行这部分工作的代码。大多数类型,比如Int32,Point,String,ArrayList 以及SerializationInfo,标识的资源并不需要特殊的清理操作。例如: 一个Point资源完全可以通过销毁对象内存中的X字段和Y字段来完成清理工作。另一方面,对于一个表示(或者说封装)着非托管(操作系统)资源(例如文件、数据库连接、套接字、互斥体、位图、图标等)的类型,在其对象被销毁的时候,就必须执行一些清理代码。
这个地方可以这样来理解: 第一: 这些资源可能占据的不仅仅是内存还包括一些系统对象,所以这些内容不是CLR所能管理的范围,是分配不了的。第二: 这些资源的内存不是在CLR中分配的,所以不能由CLR来释放。例外一点就是这些资源使用的Heap和CLR使用的Heap并不是一个。
(二)托管堆是如何知道应用程序何时不再使用某个对象的?
CLR要求所有的内存资源都从一个叫做托管堆(managed heap)的地方分配而来。
应用程序进程完成初始化后,CLR保留一块连续的地址空间,这段空间并不对应任何的物理内存,该地址空间即为托管堆。托管堆上维护着一个指针,姑且称之为NextObjPtr, 该指针标识这下一个新建对象分配时在托管堆中所处的位置。
NewObj的过程:
- 计算类型所有字段所需要的字节总数。
- 加上为对象额外附加的成员所需要的字节数。附加的字段有两个: 方法表指针和SyncBLockIndex。32系统中,各占32位,总计8个字节,64位系统中为16个字节。
- CLR检查保留区域的空间是否满足分配新对系那个所需要的字节数。满足后,对象被分配在NextObjPtr指针所在地方,在NewObj指令返回新对象的地址之前,NextObjPtr指针会越多新对象所处的内存区域,指示出新建对象在托管堆中的地址。
应用程序中同一时间分配的对象之间彼此有较强的联系,经常会在同一时间被访问,在一个垃圾收集的环境中,对象在内存中的连续分配会由于引用的本地化得到一些性能的提升,具体而言,意味着应用程序的进程工作集将会变得更小,方法中使用的对象也更有可能全部驻留在CPU的缓存中,此时对象的访问速度会提升。如果进程的工作集比较大,进程在运行时将会出现频繁的页面换入或者换出现象,这种页面的换入换出会发生在硬盘和内存之间,也会发生在内存和CPU的缓存之间。
托管堆能够获得这些好处在于做出来一个假设应用程序的地址空间和存储空间是无限的,(注:应该还是连续的,要不然如果是碎片的话,CPU的缓存就没有啥作用了),这样是不可能的,所以当应用程序调用New操作符创建对象的时候,托管堆中可能没有足够的地址空间来分配对系那个,此时需要垃圾收集。
( 三)垃圾收集算法
C#采用的垃圾收集算法是称为(generation)的机制,该机制的唯一目的就是提高垃圾收集的性能,基本思想是将应用程序生存其中新创建的对系那个认为是较新的代的一部分,而将早先创建的对象认为是较老的一部分,将对象划分为各个代龄使得垃圾收集器能够将执行对象限定在某个特定的代龄中,从而避免每次都将托管堆中的所有的对象执行垃圾收集。
C#如何知道应用程序是否正在使用某个对象呢? 每一个应用程序都有一个Root,一个Root是一个存储位置,包含着一个指向引用类型的内存指针。根有:
- 所有的全局引用类型变量或静态引用类型变量
- 线程堆栈上的所有引用类型的本地变量或者参数变量
- 在一个方法内,指向引用类型对象的CPU寄存器
当JIT编译器编译一个方法的IL代码时,除了产生本地CPU代码外,JIT编译器还会创建出来也给内部的表。从逻辑上讲,该表中的每个条目都标志着一个方法的本地CPU指令的字节偏移范围,以及该范围中一组包含根的内存地址(或者CPU寄存器)。内部表如下所示:
起始字节偏移 | 结尾字节偏移 | 根 |
0x0000000 | ox00000020 | this, arg1, arg2,ecx,edx |
this, arg2, fs, ebx | ||
fs |
如果在第二行的地址中执行垃圾回收的话,参数this, arg2,本地变量fs, 寄存器ebx都是根,他们引用的托管堆中的对象将不会被认为是可收集的垃圾对象。除此之外,垃圾收集器还可以遍历线程的调用堆栈,通过检测其中每一个方法的内部表来确定所有调用方法中的根。最后,垃圾收集器使用其他一些手段来获得存储在全局引用类型变量和静态引用类型变量中保存的根。
GC通过Root来判断对象是否可达,其得到的可达对象将包含所有的从应用程序的根可以访问的对象。任何不在该图中的对象都将是应用程序中不可访问的对象,因此也是可以被执行垃圾收集的对象。然后GC遍历垃圾对系那个的连续区块进行内存的搬迁和压缩托管堆,同时修改所有的应用程序根的指向。
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
// TODO: Implement Functionality Here
//创建1个对象,说明满了一代之后就回收的算法不是很准确
List<Int32> a = new List<Int32>();
for(Int32 x = 0; x < 1; x++)
a.Add(x);
//获取创建对象后的内存大小
long m = GC.GetTotalMemory(true);
Console.WriteLine("afte create array: {0}", m);
//执行一个循环,看什么时候会调用内存GC
for(Int32 x = 0; x < 100000; x++)
{
long curr = GC.GetTotalMemory(true);
if(m != curr)
{
Console.WriteLine("before{0}, after{1}, x is {2}", m, curr, x);
m = curr;
}
}
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}