Home >Backend Development >C#.Net Tutorial >C#.NET: Graphical code introduction to memory management story

C#.NET: Graphical code introduction to memory management story

黄舟
黄舟Original
2017-03-04 10:25:112049browse


Preface

.net运行库通过垃圾回收器自动处理回收托管资源,非托管的资源需要手动编码处理。理解内存管理的工作原理,有助于提高应用程序的速度和性能。废话少说,切入正题。主要阐述的概念见下图:




C#.NET: Graphical code introduction to memory management story


#Concept

Memory: also known as virtual memory, or virtual Address space , windows uses virtual addressing system, which automatically maps available memory addresses to actual addresses in hardware memory in the background. The result is that each process on a 32-bit processor You can use 4GB of memory to store all parts of the program, including executable code (exe files), all DLLs loaded by the code, and the contents of all variables used when the program is running.
Memory stack In the virtual memory of the process, there is an area where the lifetime of a variable must be nested.

Memory Heap In the virtual memory of the process, data is still available for a long time after the method exits.

Managed resources Resources that the garbage collector can automatically process in the background

Unmanaged resources Requires manual coding, through destructor, Finalize, Resources processed by mechanisms or methods such as IDisposable and Using.

Memory stack

Value type data is stored in the memory stack, and the instance address value of the reference type is also placed in the memory stack (see the discussion of the memory heap). The working principle of the stack can be understood through the following piece of code:

{ //block1开始
    int a;    //solve something
    {//block2开始
       int b;       // solve something else
    }//block2结束}//block1结束

Pay attention to 2 points in the above code:

1) The scope of variables in C# follows the principle that what is declared first goes out of scope, and what is declared last is first. Out of scope, that is, b is released first, a is released later, and the release order is always opposite to the order in which they allocate memory.
 2) b is in a separate block scope (
block2), and the block name where a is located is block1, which is nested with block2.  
Please see the diagram below:


C#.NET: Graphical code introduction to memory management story


## In stack memory management, a stack pointer is always maintained, which always points to the next available address in the station area, named

sp, as shown in the figure, assuming it points to the number is the address of 1000. Variables a
are first pushed onto the stack. Assume that the computer is 32-bit. The int type occupies 4 bytes, that is, 997~1000. After being pushed onto the stack, sp points to 996. It can be seen that the growth direction of the memory stack is from High address to low address. Then b is pushed onto the stack, occupying 993~996, and sp points to 992. When block block2
is exceeded, variable b immediately releases the storage on the memory stack, and sp is increased by 4 bytes, pointing to 996. When going outward and beyond block block1
, the variable a is released immediately. At this time, sp increases by 4 bytes, pointing to the original initial address of 1000. When it is pushed onto the stack later , these addresses are occupied again, and then released, and the cycle repeats.

Memory Heap

Although the stack has very high performance, it is still not very flexible for all variables, because the lifetime of variables located on the memory stack must Nested. In many cases, this requirement is too stringent because we want some data to be available long after the method exits.

As long as the heap storage space is requested using the

new
operator, the data declaration period delay is satisfied, such as all reference types. Use Managed Heap in .net to manage data on the memory heap.  The managed heap in .net is different from the heap used in C++. It works under the control of the garbage collector, while the C++ heap is low-level. Since reference type data is stored on the managed heap, how are they stored? Please look at the following code
 

void Shout()
{
   Monkey xingxing; //猴子类
   xingxing = new Monkey();
}

In this code, assume two classes, Monkey and AIMonkey, where the AIMonkey class extends the Monkey object.

 

 
Here, we call Monkey an object and xingxing an instance of it.
  First, declare a Monkey reference xingxing, and allocate storage space for this reference on the stack. Remember that this is only a reference, not the actual Monkey object. This is important to remember! ! !
Then look at the second line of code:

xingxing = new Monkey();

What it does: First, it allocates memory on the heap to store the Monkey object. Pay attention! ! ! This is a real object, it's not an address that takes up 4 bytes! ! ! Assume that the Monkey object occupies 64 bytes. These 64 bytes contain the fields of the Monkey instance and some information used to identify and manage Monkey class instances in .NET. These 64 bytes are actually allocated on the memory heap, assuming the addresses on the memory heap are 1937~2000. The new operator returns a memory address, assumed to be 997~1000, and assigned to xingxing. The schematic diagram is as follows:

C#.NET: Graphical code introduction to memory management story


记住一点:
 与内存栈不同的是,堆上的内存是向上分配的,由低地址到高地址。
 从上面的例子中,可以看出建立引用实例的过程要比建立值变量的过程更复杂,系统开销更大。那么既然开销这么大,它到底优势何在呢?引用数据类型强大到底在哪里???
 
 请看下面代码:

 {//block1
    Monkey xingxing; //猴子类
    xingxing = new Monkey();
    {//block2
      Monkey jingjing = xingxing; //jingjing也引用了Monkey对象
      //do something
    }    //jinjing超出作用域,它从栈中删除
    //现在只有xingxing还在引用Monkey}//xingxing超出作用域,它从栈中删除//现在没有在引用Monkey的了

  把一个引用实例的值xingxing赋值予另一个相同类型的实例jingjing,这样的结果便是有两个引用内存中的同一个对象Monkey了。当一个实例超出作用域时,它会从栈中删除,但引用对象的数据还是保留在堆中,一直到程序终止,或垃圾回收器回收它位置,而只有该数据不再有任何实例引用它时,它才会被删除!
  随便举一个实际应用引用的简单例子:
  

//从界面抓取数据放到list中List<Person> persons = getPersonsFromUI();
//retrieve these persons from DBList<person> personsFromDB = retrievePersonsFromDB();
//do something to personsFromDBgetSomethingToPersonsFromDB();

  请问对personsFromDB的改变,能在界面上及时相应出来吗?  
   不能!
  请看下面修改代码:

//从界面抓取数据放到list中List<Person> persons = getPersonsFromUI();
//retrieve these persons from DBList<Person> personsFromDB = retrievePersonsFromDB();
int cnt = persons.Count;for(int i=0;i<cnt;i++)
{
  persons[i]= personsFromDB [i] ;
} 
//do something to personsFromDBgetSomethingToPersonsFromDB();

 修改后,数据能立即响应在界面上。因为persons与UI绑定,所有修改在persons上,自然可以立即响应。
  这就是引用数据类型的强大之处,在C#.NET中广泛使用了这个特性。这表明,我们可以对数据的生存期进行非常强大的控制,因为只要保持对数据的引用,该数据就肯定位于堆上!!!
  这也表明了基于栈的实例与基于堆的对象的生存期不匹配!

垃圾回收器 GC

   内存堆上会有碎片形成,.NET垃圾回收器会压缩内存堆,移动对象和修改对象的所有引用的地址,这是托管的堆与非托管的堆的区别之一。
   .NET的托管堆只需要读取堆指针的值即可,但是非托管的旧堆需要遍历地址链表,找出一个地方来放置新数据,所以在.NET下实例化对象要快得多。
  堆的第一部分称为第0代,这部分驻留了最新的对象。在第0代垃圾回收过程中遗留下来的旧对象放在第1代对应的部分上,依次递归下去。。。

承上启下

  以上部分便是对托管资源的内存管理部分,这些都是在后台由.NET自动执行的。下面看下非托管资源的内存管理,比如这些资源可能是UI句柄,network连接,文件句柄,Image对象等。.NET主要通过三种机制来做这件事。分别为析构函数、IDisposable接口,和两者的结合处理方法,以此实现最好的处理结果。下面分别看一下。

析构函数

  C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于Finalize()方法的代码,并确定执行父类的Finalize()方法。看下面的代码:

public class Person
{
   ~Person()
   {      //析构实现
   }
}

~Person()析构函数生成的IL的C#代码:

protected override void Finalize()
{   try
   {      //析构实现
   }   finally
   {     base.Finalize();
   }
}

  放在finally块中确保父类的Finalize()一定调用。
  C#析构函数要比C++析构函数的使用少很多,因为它的问题是不确定性。在销毁C++对象时,其析构函数会立即执行。但由于C#使用垃圾回收器,无法确定C#对象的析构函数何时执行。如果对象占用了 宝贵的资源,而需要尽快释放资源,此时就不能等待垃圾回收器来释放了。
  第一次调用析构函数时,有析构函数的对象需要第二次调用析构函数,才会真正删除对象。如果频繁使用析构,对性能的影响非常大。

IDisposable接口

  在C#中,推荐使用IDisposable接口替代析构函数,该模式为释放非托管资源提供了确定的机制,而不像析构那样何时执行不确定。
  假定Person对象依赖于某些外部资源,且实现IDisposable接口,如果要释放它,可以这样:

class Person:IDisposable
{  public void Dispose()
  {    //implementation
  }
}

Person xingxing = new Person();//dom somethingxingxing .Dispose();

  上面代码如果在处理过程中出现异常,这段代码就没有释放xingxing,所以修改为:

Person xingxing = null;try{
   xingxing  = new Person();   //do something}finally{   if(xingxing !=null)
    {
        xingxing.Dispose();
    }
}

  C#提供了一种语法糖,叫做using,来简化以上操作。

using(Person xingxing = new Person())
{  // do something}

  using在此处的语义不同于普通的引用类库作用。using用在此处的功能,仅仅是简化了代码,这种语法糖可以少用!!!
  总之,实现IDisposable的对象,在释放非托管资源时,必须手动调用Dispose()方法。因此一旦忘记,就会造成资源泄漏。如下所示:

                Image backImage = this.BackgroundImage;                
                if (backImage != null)
                {
                    backImage.Dispose();
                    SessionToImage.DeleteImage(_imageFilePath, _imageFileName);                    
                    this.BackgroundImage = null;
                }

  在上面那个例子中,backImage已经确定不再用了,并且backImage又是通过Image.FromFile(fullPathWay)从物理磁盘上读取的,是非托管的资源,所以需要Dispose()一下,这样读取Image的这个进程就被关闭了。如果忘记写backImage.Dispose();就会造成资源泄漏!

结合 析构函数和IDisposable这2种机制

  一般情况下,最好的方法是实现两种机制,获得这两种机制的优点。因为正确调用Dispose()方法,同时把实现析构函数作为一种安全机制,以防没有调用Dispose()方法。请参考一种结合两种方法释放托管和非托管资源的机制:
  

public class Person:IDisposable
{   private bool isDisposed = false;   //实现IDisposable接口
   public void Dispose()
   {      //为true表示清理托管和非托管资源
      Dispose(true);      //告诉垃圾回收器不要调用析构函数了
      GC.SuppressFinalize(this);
   }   protected virtual void Dispose(bool disposing)
   {      //isDisposed: 是否对象已经被清理掉了
      if(!isDisposed)
      {          if(disposing)
          {            //清理托管资源
           }           //清理非托管资源
       }
       isDisposed = true;
   }

   ~Person()
   {     //false:调用后只清理非托管资源
     //托管资源会被垃圾回收器的一个单独线程Finalize()
     Dispose(false);
   }
}

  当这个对象的使用者,直接调用了Dispose()方法,比如

Person xingxing = new Person();//do somethingperson.Dispose();

  此时调用IDisposable.Dispose()方法,指定应清理所有与该对象相关的资源,包括托管和非托管资源。

  如果未调用Dispose()方法,则是由析构函数处理掉托管和非托管资源。

 以上就是C#.NET:内存管理story的图文代码介绍的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn