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
|