ASPNL logo (1 kb)
vrijdag 16 mei 2008




Microsoft MVP

.NET Codewise Community
<< vorige | overzicht | volgende >>

The Vision Column - opgeruimd staat netjes!

The Vision Column wordt verzorgd door ontwikkelaars van The Vision Web (tegenwoordig Ordina).

Door Michiel van Otegem
18 mei 2006

.NET is dan wel "managed", maar dat wil nog niet zeggen dat je helemaal niet na hoeft te denken over geheugenbeheer. Dit geldt al als je met resources werkt zoals bestanden en database connecties, en helemaal als je gaat werken met COM interop. "Hoe pak je dit aan?" is de vraag die deze editie van de column behandelt.

Zorgen dat je alles goed bijhoudt is het halve werk. Weet wanneer je met dure resources werkt, en zorg ervoor dat je die goed afsluit. .NET biedt mogelijkheden om je daarbij te helpen. Voordat we daar echter naar gaan kijken is het wel handig om te weten hoe de Garbage Collector omgaat met geheugen en het vrijgeven daarvan.

De Garbage Collector

Het geheugenbeheer wordt in .NET (voor managed code) verzorgd door de Garbage Collector (GC). Eens in de zoveel tijd, of als er een geheugen tekort is, gaat de GC kijken of er geheugen is dat vrijgegeven kan worden. Een object kan vrijgegeven worden wanneer alle referenties vrijgegeven zijn. Dat is simpel gezien wanneer alle variabelen die naar een object wijzen op null gezet zijn. Houd er rekening mee dat in complexe situaties, zoals met een collectie, dataset, enzovoorts er ook referenties zijn van binnen een object naar een ander object. Ook die moeten vrijgegeven zijn voordat de GC het ziet als een object dat opgeruimd moet worden. Als dat zo is plaatst de GC het object in de Finalizer Queue, een rij met objecten die vrijgegeven kunnen worden. Een Finalizer (ook wel destructor genoemd in C#/C++), wordt uitgevoerd om eventuele zaken op te ruimen. Ook als je geen Finalizer definieert is er een standaard Finalizer en die doet niets. Echter een object "leeft" zolang de Finalizer nog niet is uitgevoerd, ook als de Finalizer leeg is. Pas nadat de Finalizer uitgevoerd is kan de GC het geheugen vrijgeven, en dat gebeurt dan pas de volgende keer dat de GC een rondje doet over alle objecten. Als je object dus dure resources, of veel geheugen vasthoudt, wordt dit pas nadat de GC twee keer gelopen heeft vrijgegeven! Een bijkomend nadeel is dat dit allemaal niet deterministisch is, en we dus niet zeker weten waneer het geheugen daadwerkelijk vrijgegeven is.

De Garbage Collector een handje helpen

Er zijn grofweg twee manieren om de nadelen van de GC tegen te gaan, en deze dus in feite te helpen efficiënt geheugen vrij te geven. Het ene is via het zogenaamde Open-Close Design Pattern dat bijvoorbeeld gebruikt wordt door SqlConnection. De dure resource (de database verbinding) wordt pas gebruikt als je de Open –methode aanroept, en vrijgegeven zodra je de Close-methode aanroept (en dat moet je dus ook niet vergeten!). Het enige dat de GC nu nog moet doen is de rest van het object opruimen, en daarin zitten (als het goed is) geen dure resources. Het kan echter nog beter, want je kunt ervoor zorgen dat de GC een object niet in de Finalizer Queue plaatst. Hierdoor kan de GC het object meteen opruimen en het geheugen vrijgeven. Er hoeft dus niet gewacht te worden tot een volgende keer dat de GC een rondje doet langs de objecten, waardoor het het geheugen wordt hierdoor sneller vrijgegeven wordt, Uiteraard moet je er dan wel voor zorgen dat alles netjes opgeruimd is, want anders is het echt vragen op problemen. Dat doe je door de IDisposable interface te implementeren, zodat objecten gebruikt kunnen worden binnen een using-block, als volgt:

   1:  using(MyClass o = new MyClass())
   2:  {
   3:        // Hier o gebruiken
   4:  }

Deze constructie zorgt ervoor dat als het using-block verlaten wordt meteen de Dispose-method van de IDisposable interface wordt aangeroepen, en daarin geef je de dure resources vrij en geef je aan dat de GC het object niet in de Finalizer Queue hoeft te plaatsen. De implementatie van MyClass ziet er dan als volgt uit:

   1:  public class MyClass : IDisposable
   2:  {
   3:      private bool _disposed = false;
   4:   
   5:      // Administratie om bij te houden of de Dispose-method
   6:      // al aangeroepen is.
   7:      protected bool Disposed
   8:      {
   9:          get
  10:          {
  11:              lock(this)
  12:              {
  13:                  return this._disposed;
  14:              }
  15:          }
  16:      }
  17:   
  18:      // Finalizer definieren voor het geval de client niet
  19:      // de Dispose-method aanroept.
  20:      ~MyClass()
  21:      {
  22:          this.Dispose();
  23:      }
  24:   
  25:      // Methode van IDisposable interface.
  26:      public void Dispose()
  27:      {
  28:          lock(this)
  29:          {
  30:              if(this._disposed == false)
  31:              {
  32:                  CleanUp();
  33:                  this._disposed = true;
  34:   
  35:                  GC.SuppressFinalize(this);
  36:              }
  37:          }
  38:      }
  39:   
  40:      // De daadwerkelijke opruim methode. Virtual zodat sub-classes
  41:      // een overrride kunnen doen en eigen opruim code toe kunnen
  42:      // voegen.
  43:      protected virtual void CleanUp()
  44:      {
  45:          // Hier opruimen
  46:      }
  47:   
  48:      // Een methode waarin iets gedaan wordt.
  49:      public void DoWork()
  50:      {
  51:          // Voor het uitvoeren van public methods/properties
  52:          // altijd controleren of the object niet al opgeruimd is.
  53:          if(this.Disposed)
  54:          {
  55:              throw new ObjectDisposedException("Object disposed.");
  56:          }
  57:   
  58:          // Hier het werk in de methode doen.
  59:      }
  60:  }
  61:   

In de bovenstaande class wordt bijgehouden of het object al opgeruimed (disposed) is, want dat mag maar één keer gebeuren (dat is overigens geen onderdeel van de IDisposable interface, alleen de Dispose-methode behoor daartoe). Bovendien mag er daarna niets meer met het object gedaan worden, dus dit moet ook gecontroleerd worden. Om te voorkomen dat het object meerdere malen opgeruimd wordt is het ook belangrijk dat toegang tot de Dispose-method en de Disposed-property onder een lock uitgevoerd worden. Aan het eind van de Dispose-method wordt GC.SuppressFinalize() aangeroepen waarmee aan de GC wordt aangegeven dat het object niet op de Finalizer Queue geplaatst hoeft te worden, en dat het geheugen van het object dus meteen vrijgegeven kan worden.

Hoe zit het met COM objecten?

Wanneer je met COM objecten gaat werken vanuit .NET haal je een nieuw probleem naar binnen. COM objecten zijn namelijk niet managed, en houden een zogenaamde reference count bij. Dit is een teller met alle referenties die naar objecten binnen een COM object wijzen. Alleen als de reference count op 0 staat wordt het COM object daadwerkelijk vrijgegeven. Heb je bijvoorbeeld een applicatie waarin met Word gewerkt wordt, dan is zowel een referentie naar Word als naar eend document een aparte referentie, en staat de reference count dus op 2. Sluit je binnen de applicatie Word af zonder de referentie naar het document vrij te geven, dan staat de reference count nog op 1, en wordt het COM object niet goed opgeruimd. In dit geval zou dat betekenen dat Word nog in de Task Manager staat, terwijl het al afgesloten zou moeten zijn.[P1] [P2] Werk je dus met COM objecten, dan is het van uiterst belang dat je referenties vrijgeeft, als volgt:

   1:  MijnComObject obj = new MijnComObject();
   2:  IetsInMijnComObject subObj = obj.IetsInMijnComObject;
   3:   
   4:  //doe werk hier
   5:   
   6:  //Vrijgeven referenties. Dit hoeft niet met pure .NET objecten
   7:  // vanwege de GC
   8:  subObj = null;
   9:  obj = null;

Je kunt de administratie hiervan enigszins vereenvoudigen door een methode te schrijven die alle referenties verwijdert zolang de reference count groter dan 0 is. Een andere manier is om het object binnen een aparte AppDomain uit te voeren, omdat de Common Language Runtime alle referenties automatisch vrijgeeft als het AppDomain afgesloten wordt. Meer informatie over beide methodes kun je vinden in het artikel Handling Complex COM Objects with Interop Assemblies or "Why Has My Menu Button Stopped Working in Office?" van Peter Vogel

<< vorige | ^ naar boven | overzicht | volgende >>
copyright 2000-2007 ASPNL