ein Projekt von goloroden.de
Skip Navigation Linksguide to C# > Guide > Speicherverwaltung

Speicherverwaltung

Speicherverbrauch

Wird eine auf .NET basierende Anwendung ausgeführt, so wird nicht nur sie in den Speicher geladen, sondern auch die Common Language Runtime und die Klassenbibliothek von .NET. Aus diesem Grund verbraucht eine Anwendung, die auf .NET basiert, zunächst deutlich mehr Speicher als eine vergleichbare Anwendung, die beispielsweise ausschließlich auf der Win32-API aufbaut.

Seit .NET 2.0 werden die Systemkomponenten allerdings nur ein einziges Mal geladen und anschließend allen derzeit im Speicher befindlichen Anwendungen zur Verfügung gestellt, so dass der hohe Speicherbedarf bei zahlreichen gleichzeitig laufenden Anwendungen relativiert wird. Obwohl diese Maßnahme den Speicherbedarf von Anwendungen für .NET bereits deutlich gesenkt hat, scheinen sie doch übermäßig viel Speicher zu verbrauchen.

Verlässt man sich auf die Angaben, die beispielsweise der Taskmanager von Windows anzeigt, wird allerdings ein Detail des Speichermanagements von .NET übersehen: .NET reserviert für jede gestartete Anwendung zunächst zu viel freien Speicher, so dass nicht während der Ausführung der Anwendung aufwändig neuer Speicher angefordert werden muss. Der Anwendung steht also in jedem Fall genügend Speicher zur Verfügung, was der Ausführungsgeschwindigkeit zugute kommt.

Wird allerdings der Speicher im System knapp, da in der Zwischenzeit weitere Anwendungen gestartet wurden, oder da der Speicherbedarf anderer gleichzeitig ausgeführter Anwendungen gestiegen ist, gibt .NET Teile des zwar reservierten, aber ungenutzen Speichers frei. Insofern liegt der Speicherbedarf einer auf .NET basierenden Anwendung deutlich niedriger, als man zunächst annehmen könnte.

Freigabe von Ressourcen

Die aus diesem Verhalten resultierende Frage ist, warum .NET den Speicher auf diese Art verwaltet. Um diese Frage beantworten zu können, muss man wissen, was intern geschieht, wenn Typen instanziiert werden.

Bisher wurde zwischen Werte- und Verweistypen unterschieden, die entweder direkt oder indirekt im Speicher verwaltet werden. Ein weiterer Unterschied zwischen diesen Arten von Typen besteht darin, wo im Speicher Instanzen dieser Typen abgelegt werden. Während Wertetypen im sogenannten Stack abgelegt werden, werden Verweistypen auf dem sogenannten Managed Heap gespeichert, und nur ein Verweis auf diese Speicherstelle wird im Stack abgelegt.

Auffällig ist, dass Objekte in C# zwar mit Hilfe des Operators new erzeugt werden können, dass sie aber - beispielsweise im Gegensatz zu C++ - nicht wieder freigegeben werden müssen. Dies liegt daran, dass C# die Bereinigung des Speichers um nicht mehr benötigte Objekte eigenständig mit einer entsprechenden Komponente durchführt, die als Garbage Collection oder Garbage Collector bezeichnet wird.

Da es notwendig sein kann, vor dem Freigeben des Speichers, der durch ein Objekt belegt ist, einige Aufräumarbeiten auszuführen, gibt es dafür eine eigene Methode, die als Finalisierer bezeichnet wird und deren Basisimplementierung sich als Finalize in object befindet. Innerhalb dieser Methode können beispielsweise Ressourcen freigegeben werden, die nicht unter der Verwaltung von .NET stehen, wie unter anderem COM-Objekte oder Win32-Handles. Allerdings muss darauf geachtet werden, in jedem Fall den Finalisierer der Basisklasse aufzurufen.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        protected override void Finalize()
        {
            // TODO gr: Clean up any managed and unmanaged
            //          resources.
            //          2008-01-01

            // Call the base finalizer.
            base.Finalize();
        }
    }
}
Da es durchaus geschehen kann, dass der händische Aufruf des Finalisierers in der Basisklasse vergessen wird, bietet C# die Möglichkeit, analog zu einem Konstruktor eine Methode als Destruktor zu implementieren, die diesen Aufruf implizit durchführt. Ein Destruktor folgt dem gleichen Namensschema wie der Konstruktor, allerdings wird ihm eine Tilde als Präfix vorangestellt. Außerdem verfügt ein Destruktor nicht über einen Zugriffsmodifizierer. An Stelle von
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        protected override void Finalize()
        {
            // TODO gr: Clean up any managed and unmanaged
            //          resources.
            //          2008-01-02

            // Call the base finalizer.
            base.Finalize();
        }
    }
}
kann in C# also auch
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        // Finalizes this instance.
        ~Foo()
        {
            // TODO gr: Clean up any managed and unmanaged
            //          resources.
            //          2008-01-02
        }
    }
}
verwendet werden. Obwohl beide Varianten semantisch gleichwertig sind, sollte in der Praxis immer die zweite Variante verwendet werden.

Der einzige Nachteil an Destruktoren in C# ist, dass ihr Ausführungszeitpunkt nicht deterministisch ist. Sie werden dann ausgeführt, wenn die Garbage Collection den Speicher aufräumt und nicht mehr benötigte Objekte entfernt. Da die Ausführung der Garbage Collection nach einem internen Algorithmus von .NET gesteuert wird, kann man sich nicht darauf verlassen, dass ein Objekt zu einem bestimmten Zeitpunkt aufgeräumt und damit sein Finalisierer ausgeführt wird.

Die Garbage Collection kann ein Objekt jedoch nur dann freigeben, wenn sein Finalisierer ausgeführt wurde, weshalb Objekte, die über einen Finalisierer verfügen, länger im Speicher verbleiben als solche, die keinen Finalisierer enthalten. Diese Verzögerung dauert bis zur nächsten Ausführung der Garbage Collection, weshalb nur solche Klassen einen Finalisierer implementieren sollten, die nicht verwaltete Ressourcen wieder freigeben müssen.

Sollen nicht verwaltete Ressourcen zu einem vom Entwickler bestimmten Zeitpunkt oder auch verwaltete Ressourcen freigegeben werden, stellt .NET die Schnittstelle IDisposable zur Verfügung. Eine Klasse, deren Freigabeprozesse gezielt gesteuert werden sollen, muss diese Schnittstelle und die damit einhergehende Methode Dispose implementieren.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo : IDisposable
    {
        /// <summary>
        /// Disposes this instance.
        /// </summary>
        public void Dispose()
        {
            // TODO gr: Clean up any unmanaged resources.
            //          2008-01-02

            // TODO gr: Clean up any managed resources.
            //          2008-01-02
        }
    }
}
Nun kann die Methode Dispose aufgerufen werden, um die entsprechenden Ressourcen freizugeben. Allerdings kann dieser Aufruf nun wiederum vergessen werden, weshalb der Finalisierer ebenfalls Dispose aufrufen sollte.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo : IDisposable
    {
        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        ~Foo()
        {
            // Dispose this instance.
            this.Dispose();
        }

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        public void Dispose()
        {
            // TODO gr: Clean up any unmanaged resources.
            //          2008-01-02

            // TODO gr: Clean up any managed resources.
            //          2008-01-02
        }
    }
}
Doch auch diese Variante enthält einen Fehler. Wird Dispose vom Entwickler aufgerufen, so wird der Finalisierer dennoch von der Garbage Collection ausgeführt, die ihrerseits Dispose ein zweites Mal aufruft. Das heißt, es wird versucht, Ressourcen freizugeben, die längst nicht mehr belegt sind. Um dies zu verhindern, muss die Dispose-Methode den Finalisierer in der Garbage Collection abmelden, so dass dieser nicht mehr ausgeführt wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo : IDisposable
    {
        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        ~Foo()
        {
            // Dispose this instance.
            this.Dispose();
        }

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        public void Dispose()
        {
            // TODO gr: Clean up any unmanaged resources.
            //          2008-01-02

            // TODO gr: Clean up any managed resources.
            //          2008-01-02

            // Suppress execution of the finalizer for this
            // object.
            GC.SuppressFinalize(this);
        }
    }
}
Da die Garbage Collection alle verwalteten Objekte in einer beliebigen Reihenfolge aufräumt, kann es beim automatischen Aufruf von Dispose durch die Garbage Collection vorkommen, dass einige der verwalteten Ressourcen, die freigegeben werden sollen, bereits nicht mehr existieren. Um dies zu verhindern, wird eine neue Variable eingeführt, mit der überprüft werden kann, ob Dispose vom Entwickler oder von der GarbageCollection aufgerufen wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo : IDisposable
    {
        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        ~Foo()
        {
            // Dispose this instance.
            this.Dispose(false);
        }

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        /// <param name="isDisposeByUser"><c>true</c> whether
        /// disposing is called by the user; <c>false</c>
        /// otherwise.</param>
        private void Dispose(bool isDisposeByUser)
        {
            // If the disposing is called by the user,
            // managed resources may be cleaned up, too.
            if (isDisposeByUser)
            {
                // TODO gr: Clean up any managed resources.
                //          2008-01-02
            }

            // TODO gr: Clean up any unmanaged resources.
            //          2008-01-02

            // Suppress execution of the finalizer for
            // this object.
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        public void Dispose()
        {
            // Dispose this instance.
            this.Dispose(true);
        }
    }
}
Es bietet sich an, eine weitere Variable einzuführen, die festlegt, ob Dispose bereits ausgeführt wurde oder nicht, um zu verhindern, dass eine Methode noch nach dem Aufruf von Dispose ausgeführt werden soll. Geschieht dies, kann eine Ausnahme vom Typ ObjectDisposedException ausgelöst werden, der als Parameter der Name des aktuellen Objekts übergeben werden muss.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo : IDisposable
    {
        /// <summary>
        /// Contains, whether this instance has been
        /// disposed yet.
        /// </summary>
        private bool _isDisposed;

        /// <summary>
        /// Finalizes this instance.
        /// </summary>
        ~Foo()
        {
            // Dispose this instance.
            this.Dispose(false);
        }

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        /// <param name="isDisposeByUser"><c>true</c> whether
        /// disposing is called by the user; <c>false</c>
        /// otherwise.</param>
        private void Dispose(bool isDisposeByUser)
        {
            // If the disposing is called by the user,
            // managed resources may be cleaned up, too.
            if (isDisposeByUser)
            {
                // TODO gr: Clean up any managed resources.
                //          2008-01-02
            }

            // TODO gr: Clean up any unmanaged resources.
            //          2008-01-02

            // Suppress execution of the finalizer for
            // this object.
            GC.SuppressFinalize(this);

            // Define this instance as disposed.
            this._isDisposed = true;
        }

        /// <summary>
        /// Dispose this instance.
        /// </summary>
        public void Dispose()
        {
            // If this instance has been disposed, throw an
            // exception.
            if (this._isDisposed)
            {
                throw new ObjectDisposedException(
                    this.ToString());
            }

            // Dispose this instance.
            this.Dispose(true);
        }
    }
}
Prinzipiell kann eine solche Klasse wie jede andere Klasse verwendet werden, mit dem Unterschied, dass ihre Dispose-Methode aufgerufen werden sollte, sobald die Arbeit mit ihr erledigt ist.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            // Create an instance of the Foo class.
            Foo foo = new Foo();

            // TODO gr: Use the object.
            //          2008-01-02

            // Dispose the object.
            foo.Dispose();
        }
    }
}
Damit dieser Aufruf nicht vergessen wird, bietet C# eine abkürzende Schreibweise mit Hilfe des Schlüsselwortes using.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            // Create an instance of the Foo class and
            // dispose it implicitly.
            using (Foo foo = new Foo())
            {
                // TODO gr: Use the object.
                //          2008-01-02
            }
        }
    }
}

Verhalten von Zeichenketten

Neben der Art, wie .NET Speicher verwaltet, gibt es einige weitere Themen, über die ein wenig Hintergrundwissen nicht schadet. Eines dieser Themen ist die Verwaltung von Strings. Strings nehmen in .NET eine Sonderstellung ein, da sie im Speicher nicht veränderbar sind. Wird ein String verändert, wird im Hintergrund eine veränderte Kopie erzeugt, was wiederum Speicher und Zeit kostet.

Aus diesem Grund ist es nicht empfehlenswert, Strings mit Hilfe des Operators + zu verketten. Bei dem Ausdruck
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            // Concatenate some strings.
            string result =
                "Hallo" + " " + "Welt" + "!";
        }
    }
}
werden intern sieben Strings erzeugt - zunächst jeder Teilstring einzeln, dann die Kombination aus den ersten beiden, dann die Kombination aus dieser Kombination und dem dritten, und abschließend die Kombination aller Strings.

Bei einigen wenigen Strings, die miteinander verkettet werden, ist dies noch akzeptabel, ist die Anzahl aber hoch oder geschieht eine solche Verkettung innerhalb einer Schleife, so wird dadurch der Speicherbedarf unnötig in die Höhe getrieben.

Als Alternative gibt es die Klasse StringBuilder aus dem Namensraum System.Text, die einen großen Speicherbereich reserviert, in dem einzelne Strings hintereinander platziert und anschließend auf Anforderung in einen einzigen String zusammengefügt werden.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Text;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents the application class.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// Executes the application.
        /// </summary>
        public static void Main()
        {
            // Create a string builder instance.
            StringBuilder stringBuilder =
                new StringBuilder();

            // Append some strings.
            stringBuilder.Append("Hallo");
            stringBuilder.Append(" ");
            stringBuilder.Append("Welt");
            stringBuilder.Append("!");

            // Get the string from the string builder.
            string result = stringBuilder.ToString();
        }
    }
}
Obwohl das Verketten von Strings mit Hilfe der StringBuilder-Klasse deutlich schneller und speicherschonender funktioniert als auf dem klassischen Weg, muss bei ihrem Einsatz bedacht werden, dass auch hier zunächst eine Instanz erzeugt wird und Speicher reserviert werden muss, was ebenfalls Zeit kostet. Je nach Kontext gilt es also abzuwägen, auf welche Art Strings verkettet werden.

Verspätete Initialisierung

Im Zusammenhang mit statischen Konstruktoren gibt es in C# noch einen wesentlichen Aspekt zu beachten. Zunächst könnte man vermuten, die Ausführung der Klasse
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        /// <summary>
        /// Contains a bar field.
        /// </summary>
        private static int _bar = 23;
    }
}
würde analog zur Ausführung der Klasse
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        /// <summary>
        /// Contains a bar field.
        /// </summary>
        private static int _bar;

        /// <summary>
        /// Initializes the Foo type.
        /// </summary>
        static Foo()
        {
            // Set the class's fields.
            _bar = 23;
        }
    }
}
stattfinden. Es gibt allerdings einen Unterschied, der sich darin bemerkbar macht, wann die Zuweisung des Wertes an die Variable stattfindet. Während der Wert in der ersten Variante irgendwann zwischen dem Start der Anwendung und dem ersten Zugriff auf den Typ stattfindet, geschieht dies bei der zweiten Variante auf jeden Fall erst beim Zugriff auf den Typ.

Es wäre sogar ausreichend, einen vollständig leeren statischen Konstruktur bereitzustellen, der Effekt wäre der gleiche: Sobald ein statischer Konstruktor vorhanden ist, wird ein Typ erst initialisiert, wenn er tatsächlich verwendet wird.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

namespace GoloRoden.GuideToCSharp
{
    /// <summary>
    /// Represents a foo class.
    /// </summary>
    public class Foo
    {
        /// <summary>
        /// Contains a bar field.
        /// </summary>
        private static int _bar = 23;

        /// <summary>
        /// Initializes the Foo type.
        /// </summary>
        static Foo()
        {
        }
    }
}
Dies liegt daran, dass der Compiler jeden Typ mit dem internen Flag beforefieldinit kennzeichnet, der nicht über einen statischen Konstruktor verfügt. Dieses Flag bewirkt, dass der Typ irgendwann vor, spätestens aber beim ersten Zugriff initialisiert wird.

Ausnutzen lässt sich dieses Verhalten, wenn ein Typ nicht in jedem Fall in einer Anwendung benötigt wird, seine Erzeugung aber relativ aufwändig ist, weil beispielsweise auf zahlreiche externe Ressourcen zugegriffen werden muss. In einem solchen Fall kann die Initialisierung durch das Hinzufügen eines statischen Konstruktors verzögert werden, bis der Typ tatsächlich benötigt wird.