ホームページ >バックエンド開発 >C#.Net チュートリアル >C#.NET: メモリ管理ストーリーのグラフィカル コード入門

C#.NET: メモリ管理ストーリーのグラフィカル コード入門

黄舟
黄舟オリジナル
2017-03-04 10:25:112049ブラウズ


序文

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




C#.NET: メモリ管理ストーリーのグラフィカル コード入門


コンセプト

記憶: とも呼ばれます仮想メモリ、または仮想アドレス空間、Windowsはvirtualを使用しますアドレッシング システム は、使用可能なメモリ アドレスをバックグラウンドでハードウェア メモリの実際のアドレスに自動的にマップします。その結果、32 ビット プロセッサ上の各プロセスは、実行可能コード (. exe ファイル)、コードによってロードされたすべての DLL、およびプログラムの実行時に使用されるすべての変数の内容。
メモリスタック
プロセスの仮想メモリには、変数の有効期間をネストする必要がある領域があります。
メモリ ヒープ
プロセスの仮想メモリ内で、メソッドが終了した後もデータが引き続き使用できる領域。
マネージド リソース
ガベージ コレクターがバックグラウンドで自動的に処理できるリソース
アンマネージ リソース
デストラクター、Finalize、IDisposable、Using などのメカニズムまたはメソッドを通じて手動でコーディングおよび処理する必要があるリソース

メモリスタック

値型データはメモリスタックに格納され、参照型のインスタンスアドレス値もメモリスタックに配置されます(メモリスタックの動作原理の説明を参照)。 stack は、次のコード部分を通じて理解できます:

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

上記のコードの 2 つの点に注意してください:
1) C# の変数のスコープは、最初に宣言された変数はスコープ外になり、後で宣言された変数はスコープ外になるという原則に従います。スコープが最初です。つまり、b が最初に解放され、a が後に解放されます。解放順序は常に同じです。逆の順序でメモリが割り当てられます。
2) b は別のブロックスコープ (block2) にあり、a が配置されているブロック名は block1 で、その中に block2 がネストされています。

下の図を見てください:


C#.NET: メモリ管理ストーリーのグラフィカル コード入門


スタックメモリ管理では、スタックポインタが常に維持され、常にステーションエリアで次に利用可能なという名前のアドレスを指します。 sp は、図に示すように、1000 番のアドレスを指しているとします。
変数 a が最初にスタックにプッシュされると、int 型は 4 バイト、つまり 997 ~ 1000 を占有します。スタックにプッシュされた後、sp は 996 を指します。メモリスタックの成長方向は高アドレスから低アドレス方向であることがわかります。
次に、b がスタックにプッシュされ、993 ~ 996 を占め、sp は 992 を指します。ブロック block2 を超えると、変数 b は直ちにメモリ スタック上のストレージを解放し、sp は 4 バイト増加して 996 を指します。
ブロック block1 を超えて外側に進むと、変数 a はすぐに解放され、この時点で sp は 4 バイト増加し、元の初期アドレス 1000 を指します。後でスタックにプッシュされると、これらのアドレスは戻ります。再び占有され、その後解放されるというサイクルが繰り返されます。

メモリ ヒープ

スタックは非常に高いパフォーマンスを持っていますが、メモリ スタック上にある変数の有効期間をネストする必要があるため、すべての変数に対して柔軟性がまだあまり高くありません。多くの場合、メソッドが終了した後もデータを長期間使用できるようにする必要があるため、この要件は厳しすぎます。
new 演算子を使用して要求されたヒープ記憶領域が使用されている限り、すべての参照型など、データ宣言期間の遅延は満たされます。 .net の マネージド ヒープ を使用して、メモリ ヒープ上のデータを管理します。 .net のマネージド ヒープは C++ で使用されるヒープとは異なり、ガベージ コレクターの制御下で動作しますが、C++ ヒープは低レベルです。
参照型データはマネージドヒープに保存されるのですが、どのように保存されるのでしょうか?次のコードを見てください

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

このコードでは、Monkey と AIMonkey という 2 つのクラスを想定しており、AIMonkey クラスは Monkey オブジェクトを拡張しています。



ここでは、Monkey をオブジェクト、xingxing をそのインスタンスと呼びます。
まず、Monkey 参照 xingxing を宣言し、この参照にストレージ領域をスタック上に割り当てます。これは単なる参照であり、実際の Monkey オブジェクトではないことに注意してください。これは覚えておくことが重要です! ! !
次に、コードの 2 行目を見てください:

xingxing = new Monkey();

その内容: まず、Monkey オブジェクトを保存するためにヒープにメモリを割り当てます。 ! !これは実際のオブジェクトであり、4 バイトを占めるアドレスではありません。 ! ! Monkey オブジェクトが 64 バイトを占めるとします。この 64 バイトには、Monkey インスタンスのフィールドと、.NET で Monkey クラスのインスタンスを識別および管理するために使用されるいくつかの情報が含まれているとします。メモリ ヒープ上のアドレスが 1937 ~ 2000 であると仮定すると、これらの 64 バイトは実際にはメモリ ヒープ上に割り当てられます。 new 演算子は、997 ~ 1000 と想定され、xingxing に割り当てられたメモリ アドレスを返します。概略図は次のとおりです。


C#.NET: メモリ管理ストーリーのグラフィカル コード入門


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

 {//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)!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。