Maison > Article > développement back-end > C#.NET : introduction au code graphique pour l'histoire de la gestion de la mémoire
.net运行库通过垃圾回收器自动处理回收托管资源,非托管的资源需要手动编码处理。理解内存管理的工作原理,有助于提高应用程序的速度和性能。废话少说,切入正题。主要阐述的概念见下图:
Mémoire : également connue sous le nom de mémoire virtuelle, ou Espace d'adressage virtuel, Windows utilise le système d'adressage virtuel pour mapper automatiquement les adresses de mémoire disponibles aux adresses réelles de la mémoire matérielle en arrière-plan. Le résultat est que chaque adresse sur un processeur 32 bits est chaque processus. peut utiliser 4 Go de mémoire pour stocker toutes les parties du programme, y compris le code exécutable (fichiers exe), toutes les DLL chargées par le code et le contenu de toutes les variables utilisées lors de l'exécution du programme.
Pile mémoire
Dans la mémoire virtuelle du processus, il existe une zone où la durée de vie d'une variable doit être imbriquée.
Memory Heap
Dans la mémoire virtuelle d'un processus, les données sont encore disponibles longtemps après la sortie de la méthode.
Ressources gérées
Ressources que le ramasse-miettes peut traiter automatiquement en arrière-plan
Ressources non gérées
Doivent être codées manuellement via le destructeur, Finaliser, Ressources traités par des mécanismes ou des méthodes tels que IDisposable et Using.
Les données de type valeur sont stockées dans la pile mémoire, et la valeur de l'adresse d'instance du type référence est également placée dans la pile mémoire (voir la discussion du tas mémoire). Le principe de fonctionnement de la pile peut être compris à travers le morceau de code suivant :
{ //block1开始 int a; //solve something {//block2开始 int b; // solve something else }//block2结束}//block1结束
Notez 2 points dans le code ci-dessus :
1) La portée des variables en C# suit le principe de déclarer d'abord, puis de sortir de la portée, puis de déclarer d'abord hors de portée, c'est-à-dire que b est libéré en premier et a est libéré plus tard. L'ordre de libération est toujours opposé à l'ordre dans lequel ils allouent la mémoire.
2) b est dans une portée de bloc distincte (block2), et le nom du bloc où se trouve a est block1, qui est imbriqué dans block2 .
Veuillez consulter le schéma ci-dessous :
Dans la gestion de la mémoire de pile, un pointeur de pile est toujours maintenu, qui pointe toujours vers la prochaine adresse disponible dans la zone de station, nommée sp, comme le montre la figure, en supposant qu'elle pointe au numéro est l'adresse de 1000.
La variable a est d'abord poussée sur la pile. Supposons que la machine soit de 32 bits. Le type int occupe 4 octets, soit 997 ~ 1000. Après avoir été poussé sur la pile, sp points. à 996. On peut voir que la direction de croissance de la pile mémoire va de l'adresse haute à l'adresse basse.
Ensuite, b est poussé sur la pile, occupant 993~996, et sp pointe vers 992. Lorsque le bloc block2 est dépassé, la variable b libère immédiatement le stockage sur la pile mémoire, et sp augmente de 4 octets et pointe vers 996.
Lors du déplacement vers l'extérieur et au-delà du bloc block1, la variable a est libérée immédiatement. À ce moment, sp ajoute 4 octets supplémentaires, pointant vers l'adresse initiale d'origine 1000, et. puis le pousse sur la pile, ces adresses sont à nouveau occupées, puis libérées, et le cycle se répète.
Bien que la pile ait des performances très élevées, elle n'est toujours pas très flexible pour toutes les variables car la durée de vie des variables situées sur la pile mémoire doit être imbriquée. Dans de nombreux cas, cette exigence est trop stricte car nous souhaitons que certaines données soient disponibles longtemps après la fin de la méthode.
Tant que l'espace de stockage du tas est demandé à l'aide de l'opérateur new, le délai de déclaration des données est satisfait, comme pour tous les types de référence. Utilisez Managed Heap dans .net pour gérer les données sur le tas de mémoire.
Le tas géré dans .net est différent du tas utilisé par C. Il fonctionne sous le contrôle du garbage collector, tandis que le tas de C est de bas niveau.
Puisque les données de type référence sont stockées sur le tas géré, comment sont-elles stockées ? Veuillez regarder le code suivant
void Shout() { Monkey xingxing; //猴子类 xingxing = new Monkey(); }
Dans ce code, supposons deux classes, Monkey et AIMonkey, où la classe AIMonkey étend l'objet Monkey.
Ici, nous appelons Monkey un objet et xingxing une instance de celui-ci.
Tout d'abord, déclarez une référence Monkey xingxing, allouez de l'espace de stockage pour cette référence sur la pile, rappelez-vous qu'il ne s'agit que d'une référence, pas de l'objet Monkey réel. C’est important à retenir ! ! !
Regardez ensuite la deuxième ligne de code :
xingxing = new Monkey();
Ce qu'il fait : Tout d'abord, il alloue de la mémoire sur le tas pour stocker l'objet Monkey. Faites attention ! ! ! C'est un objet réel, ce n'est pas une adresse qui occupe 4 octets ! ! ! Supposons que l'objet Monkey occupe 64 octets. Ces 64 octets contiennent les champs de l'instance Monkey et certaines informations utilisées pour identifier et gérer les instances de classe Monkey dans .NET. Ces 64 octets sont en fait alloués sur le tas de mémoire, en supposant que les adresses sur le tas de mémoire sont 1937~2000. Le nouvel opérateur renvoie une adresse mémoire, supposée être 997~1000, et attribuée à xingxing. Le diagramme schématique est le suivant :
记住一点:
与内存栈不同的是,堆上的内存是向上分配的,由低地址到高地址。
从上面的例子中,可以看出建立引用实例的过程要比建立值变量的过程更复杂,系统开销更大。那么既然开销这么大,它到底优势何在呢?引用数据类型强大到底在哪里???
请看下面代码:
{//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中广泛使用了这个特性。这表明,我们可以对数据的生存期进行非常强大的控制,因为只要保持对数据的引用,该数据就肯定位于堆上!!!
这也表明了基于栈的实例与基于堆的对象的生存期不匹配!
内存堆上会有碎片形成,.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#对象的析构函数何时执行。如果对象占用了 宝贵的资源,而需要尽快释放资源,此时就不能等待垃圾回收器来释放了。
第一次调用析构函数时,有析构函数的对象需要第二次调用析构函数,才会真正删除对象。如果频繁使用析构,对性能的影响非常大。
在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();就会造成资源泄漏!
一般情况下,最好的方法是实现两种机制,获得这两种机制的优点。因为正确调用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)!