为什么要使用GC
- 提高了软件开发的抽象度
- 程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题
- 可以使模块的接口更加的清晰,减小模块间的偶合
- 大大减少了内存人为管理不当所带来的Bug
- 使内存管理更加高效
CLR
CLR是公共语言运行库(Common Language Runtime)和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集等),并保证应用和底层操作系统之间必要的分离。CLR存在两种不同的翻译名称:公共语言运行库和公共语言运行时。
运行.NET应用程序时,程序创建出来的对象实例都会被CLR跟踪,CLR都是有记录哪些对象还会被用到(存在引用关系);哪些对象不会再被用到(不存在引用关系)。CLR会整理不会再被用到的对象,在恰当的时机,按一定的规则销毁部分对象,释放出这些对象所占用的内存。
GC机制存在的问题
- GC并不是能释放所有的资源。它不能自动释放非托管资源。
- GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。
GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。
托管资源和非托管资源
托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收。
非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。
在.NET中,Object.Finalize()方法是无法重载的,编译器是根据类的析构函数来自动生成Object.Finalize()方法的,所以对于包含非托管资源的类,可以将释放非托管资源的代码放在析构函数。
内存释放的过程
CLR把没用的对象转移到一起去,使内存连续,新分配的对象就在这块连续的内存上创建,这样做是为了减少内存碎片。注意!CLR不会移动大对象
CLR规则
CLR按对象在内存中的存活的时间长短,来收集对象。时间最短的被分配到第0代,最长的被分配到第2代,一共就3代。
一般第0代的对象都是较小的对象,第2代的对象都是较大的对象,第0代对象GC收集时间最短(毫秒级别),第2代的对象GC收集时间最长。当程序需要内存时(或者程序空闲的时),GC会先收集第0代的对象,
收集完之后发现释放的内存仍然不够用,GC就会去收集第1代,第2代对象。
如果GC跑过了,内存空间依然不够用,那么就抛出了OutOfMemoryException异常。
GC跑过几次之后,第0代的对象仍然存在,那么CLR会把这些对象移动到第1代,第1代的对象也是这样。
和C++对比
CLR在运行时管理着一段内存地址空间(虚拟地址空间,在运行中会映射到物理内存地址中),分为“托管堆”和“栈”两部分,栈用于存储值类型数据,它会在方法执行结束后自动销毁其中引用的值类型变量,这一部分不属于垃圾收集的范围。托管堆用于引用类型的变量存储,是垃圾收集的关键阵地。托管堆是一段连续的地址空间,其中所分配出去的空间呈现出类似数组形态的队列结构:
NextObjPtr是托管堆所维护的一个内存指针,指示下一个对象分配的内存起始地址,它会随着内存的分配而不断移动(当然也会随着内存垃圾回收而发生移动),永远指向下一个空闲的地址。
在查找空闲内存空间时,CLR只需要在NextObjPtr处直接留出指定大小的空间提供给数据初始化,然后计算新的空闲地址并重置NextObjPtr指针即可。而在C/C++中,在分配内存之前先要遍历一遍内存占用的链表以查找合适大小的内存块,然后再修改此链表,这样也很容易产生内存碎块,使得内存分配性能下降。很明显,.NET的分配方式效率更高。但是这种效率是以GC的劳动为代价的。
如何解决循环引用
- 标记清除算法
- 标记压缩算法