[置顶] 垃圾回收GC:.Net自动内存管理 上(一)内存分配

时间:2022-02-03 00:02:06

垃圾回收GC:.Net自动内存管理 上(一)内存分配

  1. 垃圾回收GC:.Net自动内存管理 上(一)内存分配
  2. 垃圾回收GC:.Net自动内存管理 上(二)内存算法

  3. 垃圾回收GC:.Net自动内存管理 上(三)终结器


前言


.Net下的GC完全解决了开发者跟踪内存使用以及控制释放内存的窘态。然而,你或许想要理解GC是怎么工作的。此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包含非常详细的内在算法描述。同时,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理。


引子


为你的应用程序实现合理的资源管理是一件困难的,乏味的工作。这可能会把你的注意力从你当前正在解决的实际问题中转移到它身上。那么,如果有一个现有的机制为开发者管理令人厌恶的内存管理,会不会是件快意人心的事?答案是YES!在.Net中, 有一种垃圾回收机制叫GC。


每一个程序都需要使用一些计算机资源,如内存,显卡,网络,数据库等等。实际上,在一个面向对象的环境里,每一个类型都代表着程序需要使用的资源。如果要用到这些资源,则需要分配内存呈现这个类型。下面是访问这些资源的步骤:

  1. 分配内存给类型资源。
  2. 初始化内存和类型资源并使资源可用。
  3. 利用这些资源来访问类型实例成员信息(按需重复)。
  4. 销毁并清理资源
  5. 释放内存

这看起来很简单,但却是程序错误的根本来源。有多少次程序员忘记释放闲置内存?有多少次程序员试图访问已经释放的内存?

      

这两种BUG是最糟糕的情况,因为它们导制的异常结果和发生时间是不可预测的。对于其它的BUG,当你看到程序运行错误时,直接修复就行了。这两种BUG最容易造成程序资源泄漏(浪费内存)和程序对象崩溃(不稳定),而且还会促使应用程序在不可预知的时间产生不可预知的行为。当然了,有许多工具可用于跟踪监测这种BUG。

     

当我们测试GC时,你应该知道它彻底解决了开发者跟踪内存使用及确定何时释放内存的问题。然而,垃圾回收GC并不了解任何关于类型在内存中代表的资源。这意味着,GC不知道也不会去执行第四步:销毁并清理资源。在.net framework中,程序员在方法Close,Dispose,Finalize中编写有关销毁清理资源的代码,后续文章中会介绍。不过,GC能够决定什么时去自动调用这些方法。

    

有一些类型资源不需要清理。如,Rectangle类型可以通过销毁它在内存中的left,right,width和height从而被彻底清理。另一方面,一个文件类型资源或网络连接类型资源则需要非常明确的清理代码来销毁。我将解释怎么适当地完成这些任务。现在,让我们了解一下内存是怎么分配的以及资源是怎么初始化的。


内存分配



.NET CLR将所有资源分配到托管堆上,这有点像C语言中的堆但是你不用去释放资源因为闲置资源在.NET中将被自动释放。现在就有一个问题了,托管堆是怎么知道一个对象什么时候将不再被程序使用?我将简单介绍一下。
    
现今有很多的GC算法。每一个算法都针对某一特定环境进行调优,进而获得最好的性能。这篇文章着重于.NET CLR使用的GC算法。让我们从基本概念开始。
      
当一个线程初始化了,运行时将预定一块未使用的连续的地址空间。这块地址空间就是托管堆深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)。堆中同时维护着一个指针,我们叫它下一个对象指针。这个指针告诉我们下一个程序对象将被分配到堆中的什么位置。在程序初期,这个指针被设置到最基本(可以理解为第一位置)的内存地址。

程序使用new关键字创建一个新对象。这个操作首先需要确定预定的地址空间是否足够存储新对象(内存空间是否足够)。如果足够,NextObjPtr(下一个对象指针)将指向堆中的此对象,对象构造函数被调用,最后返回对象内存地址。
托管堆(对堆与栈疑惑的朋友可以参考:深入浅出图解C#堆与栈): [置顶]        垃圾回收GC:.Net自动内存管理 上(一)内存分配
(NextObjPtr:下一个对象指针)


此时,NextObjPtr将跳过此对象并指向下一个将要被存入的对象的内存地址。如上图,托管堆中有三个对象:A,B,C。下一个对象将会被放置到NextObjPtr指向的地址(即紧跟C之后)。

现在让我们看看C语言的堆怎么分配内存的。在C语言堆中,为一个对象分配内存需要通过一个数据结构链表。一旦发现一个较大的块,则进行分割块,然后链表节点中的指针需要调整修改以保证所有数据原封不动(C语言不熟,原文:In a C-runtime heap, allocating memory for an object requires walking though a linked list of data structures. Once a large enough block is found, that block has to be split, and pointers in the linked list nodes must be modified to keep everything intact. )。对.NET中的托管堆来讲,对象分配简单,只需要向指针添加一个值,相比而言这是非常快的。事实证明,在托管堆中分配一个对象几乎像在线程栈里分配内存一样快!
   

到现在为止,听起来托管堆在速度上和实现简易性上要远远地优秀于C语言的堆。但是,要使托管堆拥有这些优点需要一个大前提:地址空间和存储空间是无限大的。当然,这有些不切实际,但托管堆必须使用一些机制原理来使这个所谓的假设成立。这个机制就是垃圾回收GC。让我们看看它是怎么工作的。
      

当一个程序使用new操作符创建一个新对象时,可能没有足够的地址空间来放置它。为了检测地址空间是否足够,托管堆会偿试把对象放到NextObjPtr位置,如果NextObjPtr移动到超过地址空间边界,那说明堆已满,GC则进行垃圾回收。

   

实际上,GC会在第0代(后续文章会介绍GC中的代)被占满时进行垃圾回收。简单来说,GC中的代是GC实现的一种机制用来提高程序性能。原理上就是最新创建的对象属于GC的年轻一代,应用程序生命周期中较早创建的对象属于较老一代。把对象分成不同的代可以让GC知道要进行垃圾回收的特定代,而不是回收整个托管堆。


总结

本篇文章是为了让大家对垃圾回收GC和内存分配有一个初步的认识,不得不说了解内存分配对于一个程序员是很重要的,如果你想写高性能代码的话。虽然我们不必像使用C语言那样手工分配内存,但对内存分配茫然无知的程序员多多少少会被鄙视一点点的(只是一点点,好吧,没有任何攻击性,请不要误解)。下一篇文章将继续介绍垃圾回收GC的自动内存管理:内存算法。


翻译:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx