C#内存管理与垃圾回收
垃圾回收还得从根说起,就像生儿育女一样。
根:根是一个位置,存放一个指针,该指针指向托管堆中的一个对象,或是一个空指针不指向任何对象,即为null。根存在线程栈或托管堆中,大部分的跟都在线程栈上,因为定义的变量就存在线程栈上,类型对象指针存在托管堆中,因为实例化一个对象要额外分配两个字段“类型对象指针”和“同步块索引”。
类型对象指针的作用。实例化一个对象并没有为其方法分配内存,类型的静态字段分配内存,而实例要向调用属于类型的一些东西,就必须通过类型对象指针。如对象的实例是共用类型的方法,实例只需要通过类型对象指针调用类型的方法,更多关于方法的调用请看我的这篇博客。
同步块索引的作用。1:用于lock,使对象在同一时刻只能一个线程访问;2:用于获取对象的hashCode;3:在垃圾回收时标志某个对象是否是垃圾。关于lock最经典的一个例子就是单例了,大家的实现都是实例化一个object对象,然后锁住它,然后在判断是否要实例要实现单例的那个对象。我们为什么要实例化一个object,而不是直接lock(typeof(object)),那是因为这样会把object这个类型给锁住,锁住期间,任何使用线程使用lock(typeof(object))就必须等待,object还是可以正常使用。lock能起到单线程访问的原因是:它里面有一个空的for死循环,一直在读同步块索引中的一个位,如果这个位没有被标志跳出循环,如果被标志就一直执行循环,直到方法执行完成,其他线程就一直等待,现在你知道lock能使你的程序只能单线程反问也知道lock的效率低了吧。
NextObjPtr一个最牛B的指针。CLR中的所有资源都从托管堆中分配,托管堆是一块连续的内存空间,维护一个指针NextObjPtr,它指向上一个对象地址的后面,下一个对象的开始位置,若托管堆中没有对象就指向托管堆的开始位置,每分配一个对象就将NextObjPtr指向这个对象的后面,以准备开始分配下一个对象。NextObjptr指针移动的位置其实就是上一个对象所在空间的长度,从指向对象的开始位置改为对象的末尾吗。从哪里开始分配对象就全靠NextObjPtr啦。
实例化一个对象需要多少空间?对象的所有字段所需的内存+类型对象指针+同步块索引。关于类型对象指针和同步块索引的作用前面已经提过了。有些字段没有明显定义,但它确确实实存在,每个对象除了object的对象都有base字段,通过它可以调用父类的实例字段和方法,通过它你可以访问你爷爷的爷爷定义的字段和方法。CLR用递归的方式调用父类的方法,当然也要看,你爷爷是否愿意让你调用,原因你懂的。
在垃圾回收开始之前速度比C快。对象就这样开心的在托管堆中分配,托管堆的容量是有限的,总有一天第0代会满,容不下一粒沙子。垃圾回收就出场了,在垃圾回收出场之前,你使用内存很happy,当然速度是非常快,比C语言的速度还快,因为C的内存是随便分配,只要找到合适大小的区域,就在那里分配内存了,这样会导致内存碎片,有时需要一块大的内存,需要遍历多处。垃圾回收的时候日子就不是那么好过了。速度肯定比C慢了,看下面你就知道垃圾回收的时候,程序的速度为什么慢了。
垃圾回收分两步:1:标记;2:压缩
1:标记。在垃圾回收开始的时候,垃圾回收器视托管堆中的所有对象都为垃圾,即线程栈上没有指针指向托管堆。这样的估计是因为一个对象被视为垃圾就是它没有被引用,当垃圾回收开始的时候,垃圾回收器会沿着线程栈线性扫描,当线程栈上的一个变量引用了托管堆中的对象时,垃圾回收器就会将这个对象标记,即修改该对象同步块索引中的一个特定的位,同步块索引就是一个bit数组,每一个元素都有它特定的作用,上面就列出了我所知道的三个功能。被标记的对象也可能引用其他的对象,而被引用的对象同样会被标记,垃圾回收器是用递归的方式将这些对象一一标记的,一个对象可能会被多个对象引用,当垃圾回收器发现某个对象被标记时就会退出递归,因为再往下递归完全是多余,而且还可能出现死循环。
垃圾回收器就这样线性的扫描线程栈,递归的扫描托管堆,最后将托管堆中所有被引用的对象标记,而没有被标记的对象就是垃圾,等着被回收。
2:压缩。当垃圾被回收之后,就会出现磁盘碎片,那么就要对托管堆进行整理,即压缩。将没有被回收的对象放在一起,靠近托管堆开始的位置,将剩余的内存腾出空间来以便存放新的对象。由于压缩很多对象就会移动位置,而引用他们的指针都会变得无效,所以托管堆要修改所有指针的指向,以保证不会因为垃圾回收而让对象变得不可到达,指针变得无效。
压缩完了之后,又腾出了空间,又可以分配新的对象,当第0代满了之后又进行垃圾回收,垃圾回收就这样一直进行着,直到回收了3代还是没有内存可以分配,那就是弹尽粮绝的时候了,CLR会告诉你OutOfMemoryException。CLR的内存被的程序吃光了。更多关于代的信息,可以看我的这篇博客。在第0代满的时候就会进行垃圾回收,第0代回收完之后还是没有足够的内存存放当前对象就回收第1代,如果还是不够就回收第2代,够就不回收下一代,垃圾回收还可以用代码控制GC.Collect()。
更多关于内存管理和垃圾回收的内容,请等待我的下一篇博客。