Stack en Heap
Geheugenmanagement in C-Sharp
Tot nog toe lagen we er niet van wakker wat er achter de schermen van een C# programma gebeurde. We duiken nu dieper in wat er juist gebeurt wanneer we variabelen aanmaken.
Twee soorten geheugen
Wanneer een C# applicatie wordt uitgevoerd krijgt het twee soorten geheugen toegewezen dat het 'naar hartelust' kan gebruiken:
Het kleine, maar snelle stack geheugen
Het grote, maar tragere heap geheugen
Afhankelijk van het soort variabele wordt ofwel de stack, ofwel de heap gebruikt. Het is uitermate belangrijk dat je weet in welk geheugen de variabele zal bewaard worden!
Er zijn namelijk twee soorten variabelen:
Value types
Reference types
Als je volgende tabel begrijpt dan beheers je geheugenmanagement in C#:
Value types | Reference types | |
Inhoud van de variabele | De eigenlijke data | Een referentie naar de eigenlijke data |
Locatie | (Data) Stack | Heap (globaal)geheugen |
Beginwaarde |
|
|
Effect van = operator | Kopieert de actuele waarde | Kopieert het adres naar de actuele waarde |
Waarom twee geheugens?
Waarom plaatsen we niet alles in de stack? De reden hiervoor is dat bij het compileren van je applicatie er reeds zal berekend worden hoeveel geheugen de stack zal nodig hebben. Wanneer je programma dus later wordt uitgevoerd weet het OS perfect hoeveel geheugen het minstens moet reserveren. Er is echter een probleem: we kunnen niet alles perfect berekenen/voorspellen. Een variabele van het type int
is perfect geweten hoe groot die zal zijn (32 bit).Maar wat met een string? Of met een array waarvan we pas tijdens de uitvoer de lengte aan de gebruiker misschien vragen? Het zou nutteloos (en zonde) zijn om reeds bij aanvang een bepaalde hoeveelheid voor een array te reserveren als we niet weten hoe groot die zal worden. Beeld je maar eens in dat we 2k byte reserveren om dan te ontdekken dat we maar 5byte ervan nodig hebben. RAM is goedkoop, maar toch... De heap laat ons dus toe om geheugen op een wat minder gestructureerde manier in te palmen. Tijdens de uitvoer van het programma zal de heap als het ware dienst doen als een grote zandbak waar eender welke plek kan ingepalmd worden om zaken te bewaren. De stack daarentegen is het kleine bankje naast de zandbak: handig, snel, en perfect geweten hoe groot.
Value types
Value types worden in de stack bewaard. De effectieve waarde van de variabele wordt in de stack bewaard. Dit zijn alle gekende, 'eenvoudige' datatypes die we totnogtoe gezien hebben, inclusief enums en structs (zie later):
sbyte
,byte
short
,ushort
int
,uint
long
,ulong
char
float
,double
,decimal
bool
structs (zien we niet in deze cursus)
enums
= operator bij value types
Wanneer we een value-type willen kopieren dan kopieren de echte waarde:
Vanaf nu zal anderGetal
de waarde 3
hebben. Als we nu een van beide variabelen aanpassen dan zal dit geen effect hebben op de andere variabelen.
We zien het zelfde effect wanneer we een methode maken die een parameter van het value type aanvaardt- we geven een kopie van de variabele mee:
De parameter a
zal de waarde 5 gekopieerd krijgen. Maar wanneer we nu zaken aanpassen in a
zal dit geen effect hebben op de waarde van getal
. De output van bovenstaand programma zal zijn:
Reference types
Reference types worden in de heap bewaard. De effectieve waarde wordt in de heap bewaard, en in de stack zal enkel een referentie of pointer naar de data in de heap bewaard worden. Een referentie (of pointer) is niet meer dan het geheugenadres naar waar verwezen wordt (bv 0xA3B3163
) Concreet zijn dit alle zaken die vaak redelijk groot zullen zijn:
objecten, interfaces en delegates
arrays
= operator bij reference types
Wanneer we de = operator gebruiken bij een reference type dan kopieren we de referentie naar de waarde, niet de waarde zelf.
Bij objecten We zien dit gedrag bij alle reference types, zoals objecten:
Wat gebeurt er hier?
new Student()
:new
roept de constructor vanStudent
aan. Deze zal een constructor in de heap aanmaken en vervolgens de geheugenlocatie teruggeven.Een variabele
stud
wordt in de stack aangemaakt.De geheugenlocatie uit de eerste stap wordt vervolgens in
stud
opgeslagen in de stack.
Bij arrays
Maar ook bij arrays:
In dit voorbeeld zal andereNummers
dus nu ook verwijzen naar de array in de heap waar de actuele waarden staan.
Als we dus volgende code uitvoeren dan ontdekken we dat beide variabele naar dezelfde array verwijzen:
We zullen dus als output krijgen:
Hetzelfde gedrag zien we bij objecten:
We zullen in dit geval dus Queen
op het scherm zien omdat zowel b
als a
naar het zelfde object in de heap verwijzen. Het originele "abba"-object zijn we kwijt en zal verdwijnen (zie Garbage collector verderop).
Methoden en reference parameters
Ook bij methoden geven we de dus de referentie naar de waarde mee. In de methode kunnen we dus zaken aanpassen van de parameter en dan passen we eigenlijk de originele variabele aan:
We krijgen als uitvoer:
Opgelet: Wanneer we een methode hebben die een value type aanvaardt en we geven één element van de array met dan geven dus een kopie van de actuele waarde mee!
De output bewijst dit:
De Garbage Collector (GC)
Een essentieel onderdeel van .NET is de zogenaamde GC, de Garbage Collector. Dit is een geautomatiseerd onderdeel van ieder C# programma dat ervoor zorgt dat we geen geheugen voor niets gereserveerd houden. De GC zal geregeld het geheugen doorlopen en kijken of er in de heap data staat waar geen references naar verwijzen. Indien er geen references naar wijzen zal dit stuk data verwijderd worden.
In dit voorbeeld zien we dit in actie:
Vanaf de laatste lijn zal er geen referentie meer naar {3,4,5}
in de heap zijn, daar we deze hebben overschreven met een referentie naar {1,2,3}
.De GC zal dus deze data verwijderen.
Wil je dat niet dan zal je dus minstens 1 variabele moeten hebben dat naar de data verwijst. Volgende voorbeeld toont dit:
De variabele bewaarArray
houdt dus een referentie naar {3,4,5}
bij en we kunnen dus later via deze variabele alsnog aan de originele data.
Meer weten?
Meer info, lees zeker volgende artikels:
Last updated