>  기사  >  백엔드 개발  >  .NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)

.NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)

黄舟
黄舟원래의
2017-03-18 13:34:271290검색

서문

rreee

.NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)

개념

메모리:라고도 함 가상 메모리 또는 가상 주소 공간, Windows는 가상 주소 지정 시스템을 사용하여 백그라운드에서 사용 가능한 메모리 주소를 하드웨어 메모리의 실제 주소에 자동으로 매핑합니다. 32비트 프로세서의 각 프로세스는 실행 코드(exe 파일), 코드에 의해 로드된 모든 DLL 및 프로그램 실행 시 사용되는 모든 구성 요소를 포함하여 프로그램의 모든 부분을 저장하기 위해 4GB의 메모리를 사용할 수 있습니다. 변수의.
메모리 스택
프로세스의 가상 메모리에는 변수의 수명이 중첩되어야 하는 영역이 있습니다.
메모리 힙
프로세스의 가상 메모리에서는 메서드가 종료된 후에도 오랫동안 데이터를 사용할 수 있습니다.
관리되는 리소스
가비지 컬렉터가 백그라운드에서 자동으로 처리할 수 있는 리소스
관리되지 않는 리소스
소멸자, Finalize, Resources를 통해 수동으로 코딩해야 함 IDisposable 및 Using와 같은 메커니즘이나 방법으로 처리됩니다.

메모리 스택

값 유형 데이터는 메모리 스택에 저장되며, 참조 유형의 인스턴스 주소 값도 메모리 스택에 배치됩니다(토론 참조). 스택의 작동 원리는 다음 코드를 통해 이해할 수 있습니다.

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

위 코드에서 2가지 사항에 주의하세요.
1) C#의 변수 범위 먼저 선언된 것이 범위를 벗어나고, 마지막에 선언된 것이 먼저 범위를 벗어난다는 원칙을 따릅니다. 즉, b가 먼저 해제되고, a가 나중에 해제되며, 해제 순서는 항상 반대 순서입니다. 그들은 메모리를 할당합니다.
2) b는 별도의 블록 범위(block2)에 있고, a가 위치한 블록 이름은 block1이며, block2 내에 중첩되어 있습니다. .
 
아래 도표를 참고하세요.


.NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)

스택 메모리 관리에서는 항상 유지 관리되는 스택 포인터는 그림과 같이 역 영역에서 사용 가능한 다음 주소 sp를 항상 가리키는데, 주소 번호 1000을 가리킨다고 가정합니다.
변수 a가 먼저 스택에 푸시됩니다. 머신이 32비트라고 가정합니다. int 유형은 스택에 푸시된 후 997~1000을 차지합니다. to 996. 메모리 스택의 성장 방향이 높은 주소에서 낮은 주소로 향함을 알 수 있다.
그러면 b가 스택에 푸시되어 993~996을 차지하고 sp는 992를 가리킵니다. 블록 block2이 초과되면 변수 b는 즉시 메모리 스택의 저장소를 해제하고 sp는 4바이트 증가하여 996을 가리킵니다.
블록 block1 밖으로 이동하면 변수 a가 즉시 해제됩니다. 이때 sp는 원래 초기 주소 1000을 가리키는 또 다른 4바이트를 추가하고, 그런 다음 스택에 푸시하면 이 주소가 다시 점유된 다음 해제됩니다. 주기.

메모리 힙

스택의 성능은 매우 높지만 메모리 스택에 있는 변수의 수명은 중첩되어야 하기 때문에 여전히 모든 변수에 대해 유연성이 떨어집니다. 대부분의 경우 이 요구 사항은 메서드가 종료된 후에도 오랫동안 일부 데이터를 사용할 수 있기를 원하기 때문에 너무 엄격합니다.
new 연산자 를 사용하여 요청한 힙 저장 공간만 만족하면 모든 참조 유형 등 데이터 선언 주기 지연을 만족한다. .net에서 관리되는 힙을 사용하여 메모리 힙의 데이터를 관리하세요.
.net의 관리형 힙은 C++에서 사용되는 힙과 다르며, C++ 힙은 하위 수준입니다.
참조형 데이터는 관리되는 힙에 저장되는데, 어떻게 저장되나요? 다음 코드를 살펴보세요
 

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

이 코드에서는 Monkey와 AIMonkey라는 두 클래스를 가정합니다. 여기서 AIMonkey 클래스는 Monkey 객체 를 확장합니다.
 
 여기에서는 Monkey를 객체로 부르고 xingxing을 인스턴스로 부릅니다.
 
먼저 Monkey 참조 xingxing을 선언하고 이 참조에 대한 저장 공간을 스택에 할당합니다. 이는 실제 Monkey 개체가 아니라 참조일 뿐이라는 점을 기억하세요. 이것을 기억하는 것이 중요합니다! ! !
그런 다음 두 번째 코드 줄을 살펴보세요.

xingxing = new Monkey();

  它完成的操作:首先,它分配堆上的内存,以储存Monkey对象,注意了!!!这是一个真正的对象,它不是一个占用4个字节的地址!!!  假定Monkey对象占用64个字节,这64个字节包含了Monkey实例的字段,和.NET中用于识别和管理Monkey类实例的一些信息。这64个字节实在内存堆上分配的,假定内存堆上的地址1937~2000。new操作符返回一个内存地址,假定为997~1000,并赋值给xingxing。示意图如下所示:


.NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)

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

 {//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()方法,则是由析构函数处理掉托管和非托管资源。

위 내용은 .NET Framework - 메모리 관리 스토리와 변수 생성 및 소멸에 대한 자세한 설명(그림)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.