Stack vs. Heap

Während ein Programm ausgeführt wird, müssen Daten im Speicher abgelegt werden. Der benötigte Speicherplatz sowie die Ablageposition der Daten werden durch den jeweiligen Datentyp festgelegt. Beim Programmablauf werden zwei Speicherbereiche zur Datenhaltung verwendet: Stack (Stapelspeicher) und Heap (Haldenspeicher).

Stack (Stapelspeicher)

Der Stack ist ein zusammenhängendes Speicherarray, das dem Datenstrukturprinzip Last In, First Out (LIFO) – zuletzt abgelegt, zuerst entnommen folgt. Er speichert drei Arten von Daten:

  • Werte von Variablen bestimmter Typen
  • Aktueller Ausführungskontext des Programms
  • An Methoden übergebene Parameter
Der Stack lässt sich mit einer Münzröhre vergleichen: Daten lassen sich nur an einem Ende einfügen und herausnehmen.

Alle Speichervorgänge des Stacks werden automatisch vom System verwaltet, der Entwickler muss keine manuellen Eingriffe vornehmen. Das Verständnis der zugrundeliegenden Logik des Stacks erleichtert das Nachvollziehen des Programmablaufs sowie das Lesen der offiziellen C#-Dokumentation erheblich.

Kernmerkmale des Stacks

  • Hinzufügen und Entfernen von Daten ist nur an der Stackspitze möglich
  • Daten auf die Stackspitze legen: Push (Aufstapeln)
  • Daten von der Stackspitze entnehmen: Pop (Abladen)

Eigenschaften des Stack-Speichers

  • Extrem schnelle Speicherzuweisung und -freigabe, nutzt zusammenhängende Speicherblöcke
  • Lebenszyklus an Methodenaufrufe gebunden: Ein Stack-Frame wird automatisch zerstört, sobald die Methode abgearbeitet ist
  • Gespeicherte Inhalte: Werttyp-Variablen, Methodenparameter, lokale Variablen, Ausführungskontext

Um die Funktionsweise des Heaps besser nachvollziehen zu können, betrachten wir ein einfaches Codebeispiel

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code-Sprache: JavaScript (javascript)

Die Logik dieses Beispiels ist unkompliziert. Ab der ersten Zeile der Einstiegsfunktion Main wird eine Integer-Variable a erstellt, in der zweiten Zeile folgt die Variable b. Zeile drei ruft die Funktion Add auf und übergibt die Werte von a und b an die Funktion. Innerhalb der Funktion erhält x den Wert von a, y den Wert von b; die Funktion gibt das Ergebnis von x + y, also 30, an die Aufrufstelle zurück und weist es sum zu. Sobald Add abgeschlossen ist, werden x und y vom Stack abgeladen, sum erhält den Wert 30; nach Beendigung von Main wird das Programm beendet. Im Folgenden gehen wir Schritt für Schritt den gesamten Push- und Pop-Vorgang im Stack durch.

Programmablauf und Push/Pop-Vorgänge des Stacks

Beim Erstellen der Variable a werden eingebaute Grundtypen direkt im Stack abgelegt, der Vorgang ist ein Push
Ausführung der zweiten Codezeile, Erstellung der zweiten Integer-Variable b mit dem Wert 20. Zuerst abgelegte Daten liegen am Stackboden, später hinzugefügte an der Stackspitze
In der dritten Zeile wird zunächst Add(a,b) rechts vom Gleichheitszeichen ausgeführt, um die Funktion aufzurufen. Die Werte von a und b werden an x und y kopiert – es handelt sich um eine Wertübergabe, a und x sind voneinander unabhängige Variablen ohne gegenseitige Beeinflussung.
Bei diesem Schritt wird lediglich x + y berechnet und das Ergebnis zurückgegeben, es gibt keine dauerhafte Ablage im Stack. Der Wert 30 gelangt zurück zur Aufrufstelle, vergleichbar mit der Anweisung int sum = 30;. Nach Ausführung von return x+y schließt sich die Funktion Add, alle lokalen Variablen der Funktion werden per Pop entfernt; der belegte Speicherbereich wird gelöscht und für spätere Nutzung freigegeben.
Die oberste Position des Stacks heißt Stackspitze (Top); nach einem Pop rückt die Spitzenmarkierung nach unten.
Nach Abschluss von Add sind x und y lokale Parameter dieser Funktion, sie werden nacheinander per Pop entfernt, um Stack-Speicherplatz freizugeben und einen flüssigen Systemablauf zu gewährleisten.
Nach dem Abladen existieren x und y nicht mehr; der von Add zurückgelieferte Wert 30 gelangt zur Aufrufstelle und wird sum zugewiesen.
Die Anweisung int sum = 30 löst erneut einen Push-Vorgang aus, die Variable sum mit dem Wert 30 wird an die Stackspitze gelegt. Nach dieser Zeile steht Main vor dem Abschluss, die lokalen Variablen sum, b und a werden nacheinander per Pop zerstört.
Pop-Vorgänge laufen stets von der Stackspitze aus ab. Der Stack ähnelt einem Gang mit nur einem Ein- und Ausgang, alle Daten werden durch dasselbe Ende eingefügt und entnommen. Daher werden die Variablen in Main in umgekehrter Erstellreihenfolge gelöscht: zuerst sum, dann b, zuletzt a.
Die Variable b wird per Pop entfernt und zerstört, die Markierung der Stackspitze rückt automatisch nach unten.
Alle Variablen wurden per Pop entfernt, auch die letzte Variable a. Streng genommen besitzt Main noch eine weitere lokale Variable: das String-Array args aus Main(string[] args), das beim Konsolenstart übergebene Kommandozeilenargumente aufnimmt. Die Daten dieses Arrays verteilen sich auf zwei separate Speicherbereiche, ein Thema, das mit dem Heap zusammenhängt und wir hier zur Vereinfachung der Demo ausklammern.

Merken Sie sich einen zentralen Punkt: Beim Ausführen einer Funktion werden Variablen von oben nach unten per Push auf den Stack gelegt, die Reihenfolge lautet a → b → Eintritt in Add → x → y. Nach Ende von Add werden x und y abgeladen, anschließend sum aufgestapelt; wenn Main schließt, folgen die Löschungen in umgekehrter Erstellreihenfolge: sum, b, a.


Heap (Haldenspeicher)

Der Heap ist ein großer Speicherbereich, der zur dynamischen Zuweisung von Speicherblöcken für Datenobjekte dient. Im Gegensatz zum Stack lassen sich Daten im Heap in beliebiger Reihenfolge einfügen und entfernen.

Ein Programm kann Objekte im Heap nicht manuell löschen. Der Garbage Collector (GC) der CLR (Common Language Runtime) räumt automatisch verwaiste, nicht mehr referenzierbare Objekte auf. Dadurch entfallen die fehleranfälligen manuellen Speicherfreigaben, die bei anderen Programmiersprachen erforderlich sind.

Der Heap lässt sich mit einem großen Schreibtisch vergleichen, auf dem man Gegenstände frei platzieren kann. Der zuvor beschriebene Stack hingegen gleicht einer Münzröhre mit nur einem Zugriffsend.

Der Heap ist wie ein weiter Schreibtisch ohne Platzbeschränkungen für abgelegte Objekte. Der Entwickler muss Heap-Objekte nicht manuell entfernen, die Verwaltung übernimmt der GC. Sobald kein Teil des Programms mehr auf ein Objekt verweist, zerstört der GC es in unregelmäßigen Abständen und gibt den Speicher frei. Das entlastet Entwickler deutlich und verhindert Programmabstürze durch nicht freigegebene Speicherlecks. Sprachen wie C++ erfordern hingegen eine manuelle Freigabe des gesamten Heap-Speichers durch den Programmierer.

Ablauf der GC-Speicherbereinigung

  1. Das Programm erstellt drei Objekte im Heap und hält gültige Referenzen darauf
  2. Das Programm nutzt eines der Objekte nicht mehr, alle Referenzen darauf gehen verloren
  3. Der GC erkennt das referenzlose verwaiste Objekt und gibt seinen belegten Speicher frei
  4. Bereinigung abgeschlossen, der freigegebene Speicher kann neuen Objekten zugewiesen werden

Eigenschaften des Heap-Speichers

  • Nicht zusammenhängender Speicher, Zuweisung läuft langsamer als beim Stack
  • Lebenszyklus ist nicht an Methodengrenzen gebunden, zentral verwaltet durch den GC
  • Gespeicherte Inhalte: Instanzen von Referenztypen (Klassen, Arrays, Delegaten etc.); im Stack werden nur die Speicheradressen der Heap-Objekte abgelegt

Ein praktisches Beispiel verdeutlicht diesen Vorgang. Wie bereits erläutert, liegen lokale Variablen von Grundtypen im Stack, während benutzerdefinierte Typen üblicherweise im Heap abgelegt werden. Sehen Sie sich folgenden Code an:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //Membervariablen
    private string name;

    private int age;

    //Konstruktor, wird später detailliert erklärt
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //Membermethode
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code-Sprache: JavaScript (javascript)

Die folgende Abbildung zeigt die Speicherablage der vier Variablen a, b, tom und lucy innerhalb von Main. Wie bei der Stack-Erläuterung läuft der Code von oben nach unten ab: zuerst wird a auf den Stack gepusht, danach b. Die Werte von a und b liegen direkt im Stack.

Variablen von Grundtypen werden direkt im Stack gespeichert
Die eigentlichen Daten des benutzerdefinierten Typs Student liegen im Heap, im Stack wird lediglich die Speicheradresse des Heap-Bereichs abgelegt.
Beispielsweise liegen die Daten von tom im Heap-Block 00001, der Stack speichert die Adresse 00001, um später auf die Daten zugreifen zu können.

Sobald die Funktion Main abgeschlossen ist, wird die Adressreferenz lucy: 01010 im Stack per Pop entfernt und zerstört. Gelöscht wird aber nur die Adresse, nicht die eigentlichen Objektdaten im Heap. An diesem Punkt verliert das Heap-Objekt alle Referenzaufzeichnungen aus den Stack-Variablen.

Das Lucy-Objekt im Heap-Block 01010 hat keine Stack-Variable mehr, die seine Adresse speichert – es wird zu einem verwaisten Objekt. Der Garbage Collector GC zerstört es zu einem passenden Zeitpunkt.
Das Lucy-Objekt im Heap verliert alle externen Referenzen, sobald seine im Stack abgelegte Adresse per Pop entfernt wird. Der GC löscht den Speicher dieses Objekts zu einem beliebigen Zeitpunkt, um den Bereich wiederverwendbar zu machen. Solange irgendeine Stelle im Programm noch auf das Objekt verweist, wird es vom GC nicht zerstört.

Speicherbereiche im Überblick

Stack und Heap sind logische Speicherunterteilungen auf Softwareebene, keine physischen Hardwarepartitionen.

Stack und Heap stellen lediglich softwareseitige logische Aufteilungen des Speichers dar; der physische RAM-Hardware verfügt über keine dedizierten Hardwaremodule mit der Kennzeichnung „Stack-Bereich“ oder „Heap-Bereich“.

Der physische RAM besteht nur aus einheitlichen Speicherchips mit aufeinanderfolgenden physischen Adressen, es gibt keine Hardware-Markierungen zur Unterscheidung von Stack und Heap

Das Betriebssystem weist jedem laufenden Prozess einen separaten virtuellen Speicherbereich zu und unterteilt diesen virtuellen Speicher im Vorfeld in vier zentrale logische Segmente:

Code-Segment: Speichert kompilierte Programmanweisungen

Globales statisches Segment: Statische Variablen, Konstanten

Stack-Segment: Jeder Hauptthread und Unterthread des Programms besitzt einen eigenen unabhängigen Stack-Speicherblock

Heap-Segment: Großer gemeinsam genutzter Speicherpool für dynamische Zuweisungen

Der hier beschriebene Stack und Heap sind lediglich logische Bereiche, die das Betriebssystem jedem Prozess zuweist; es gibt keine hardwareseitige Trennung zwischen ihnen.

Stack vs. Heap

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert