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.
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
}
}
}
}
|
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.
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.