AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Delphi Der .NET Garbage Collector
Tutorial durchsuchen
Ansicht
Themen-Optionen

Der .NET Garbage Collector

Ein Tutorial von sakura · begonnen am 2. Dez 2004 · letzter Beitrag vom 2. Dez 2004
Tutorial geschlossen
Benutzerbild von sakura
sakura
Registriert seit: 10. Jun 2002
Hi DPler,

nachdem die vor ein paar Tagen die Frage aufkam, wie Interfaces unter .NET freigegeben werden, habe ich mal angefangen ein Beispiel zusammenzusetzen. Im folgenden werde ich verschiedene Punkte des .NET Garbage Collectors (kurz: GC) näher betrachten. Das ist in keiner Hinsicht eine vollständige Betrachtung dessen, da mir dazu momentan die Zeit fehlt. Ich hoffe, Euch jedoch einen kurzen Einblick geben zu können.

Euer Sakura.

Wer über diesen Artikel diskutieren möchte, möge das hier tun: Interfaces in DotNet ganz anders? Somit bleibt das Tutorial ein Tutorial und kein Antwort-Fragen Thread

......
Ich bin nicht zurück, ich tue nur so
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#2
  Alt 2. Dez 2004, 13:41
Interfaces in Delphi für Win32

Bevor ich auf die Interface von .NET eingehe, möchte ich noch einmal kurz die Interfaces erläutern, wie diese unter Delphi für Win32 durch Borland implementiert wurden.

Alle in Delphi erstellten Interfaces wurden unter Delphi von Delphi-Referenz durchsuchenIInterface abgeleitet. IInterface ist wie folgend deklariert:
Delphi-Quellcode:
type
  IInterface = interface
    ['{00000000-0000-0000-C000-000000000046}']
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

  IUnknown = IInterface;
Uns interessieren jetzt besonders die Methoden _AddRef und _Release. Diese wurden von Delphi genutzt, um ein automatisches Referenz-Counting zu implementieren. Kurz heißt das, wann immer ein Objekt (ob TObject- oder IInterface-Abkömmlinge) einer Interface-Variablen zugewiesen wird, hat Delphi intern automatisch den Referenzzählen erhöht (_AddRef). Wenn immer eine Interface-Variable durch eine neues Objekt überschrieben wurde, bzw. mit nil gelöscht wurde, bzw. der Zugriffsbereich der Variable verlassen wurde, hat Delphi automatisch einen Aufruf zu _Release eingefügt. Wurde jetzt die letzte Referenz frei gegeben, _Release liefert den Wert 0 zurück, hat die Objekt-Implementierung von _Release das Objekt selbständig freigegeben. Das Standardbeispiel:
Delphi-Quellcode:
function TInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;
Diese Vorgehensweise von Delphi hat das Arbeiten mit Interfaces sehr leicht gemacht, es hatte aber auch seine Tücken. Man sollte ein Objekt besser nie sowohl über Objekt-Variablen als auch über Interface-Variablen ansprechen. Sonst konnte es zu verschiedenen Zugriffsverletzungen (AV) kommen. Ein kurzes Beispiel:
Delphi-Quellcode:
var
  Obj: TSomeInterfacedObject;
  Intf: ISomeInterface;
[...]
Obj := TSomeInterfacedObject.Create;
Intf := Obj;
[...]
Obj.SomeMethod;
Intf.SomeInterfacedMethod;
Intf := nil;
[...]
Obj.SomeMethod;
[...]
In Zeile 5 erstellen wir ein Objekt vom Typ TSomeInterfacedObject, welches das Interface ISomeInterface implementiert. Jetzt wird in Zeile 6 der Variable Intf eine Referenz auf das Interface ISomeInterface, implementiert durch Obj übergeben. Hier erhöht Delphi automatisch den Interface-Referenz-Counter, dieser wird jetzt 1. Zur Zeit sind beide Instanzen (Obj und Intf) gültig. Somit ist der Zugriff in Zeile 8 auf Obj.SomeMethod kein Problem, genauso kann in Zeile 9 auch auf Intf.SomeInterfacedMethod; zugegriffen werden.

Nun wir in Zeile 10 der Interface-Verweis (Intf) auf nil gesetzt, also gelöscht. Delphi implementiert jetzt automatisch den Aufruf zu _Release und das Objekt dekrementiert seinen Referenz-Counter auf 0. Damit gibt es keine Interface-Variablen mehr, welche auf das Objekt verweisen und dieses gibt sich selbständig frei!

In Zeile 12 greifen wir mit obj.SomeMethod nun wieder auf das bereits zerstörte Objekt zu. Je nach Aktion ist der weitere Verlauf der Applikation jetzt unvorhersehbar. Im besten Falle erkennt Windows das Problem und meldet eine Zugriffsverletzung und bricht die aktuelle Verarbeitung mit einer Fehlermeldung ab.
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#3
  Alt 2. Dez 2004, 14:13
Interfaces im Windows .NET Framework

Abgesehen vom Namen und der Idee (Definition, aber nicht Implementierung) und der Syntax haben die .NET Interfaces mit den Delphi für Win32 Interfaces nicht mehr viel gemeinsam.

Das Referenz-Counting der Interfaces wird vollständig von Delphis Schultern genommen und durch das Referenz-Counting von .NET übernommen. Der erste sehr große Vorteil ist: Objekte und Interfaces dieser Objekte werden gemeinsam gezählt. Kurz gesagt: obiger Code, welcher in Win32 zu einer AV führen kann, funktioniert unter .NET einwandfrei.

In .NET übernehmen Objekte die Referenzzählung nicht mehr selbständig, wie es unter Win32 nötig war. In .NET wird diese durch den .NET eigenen Garbage Collector (GC) übernommen. Die genauen Interna des GC werde ich zu späterer Zeit, in einem anderen Tutorial, erläutern. Momentan soll ein kurzer Auszug reichen.

Wird unter .NET die Instanz einer Klasse erstellt (z.B. TObject.Create;), also ein Objekt, so erfolgt das in der Intermediate Language (IL, der .NET Bytecode) durch einen Aufruf zu newobj. newobj berechnet dann die Menge an Speicher, welche durch das neue Objekt benötigt wird. Anschließend wird die Menge an Speicher hinzugefügt, die .NET zur Verwaltung des Objektes (Overhead) benötigt. Je nach System sind das zur Zeit 8 Bytes (32 Bit-Systeme) oder 16 Bytes (64 Bit-Systeme). .NET nutzt ein internes Referenz-Counting. Es ist natürlich intern etwas komplizierter als hier dargestellt, aber das soll für dieses Tutorial genügen.

Wird eine Referenz auf ein Objekt oder ein Interface gelöscht (oder überschrieben), so wird der Referenzzähler des Objektes, bzw. des Objektes welches das Interface implementiert, um eins verringert.

Hier liegt der Unterschied, unter Delphi für Win32 hat das Objekt nur für Interfaces intern einen eigenen Refernz-Counter genutzt. In .NET verwaltet das System selbst einen Referenz-Counter für das Objekt, unabhängig ob auf dieses direkt oder über ein Interface zugegriffen wird.
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#4
  Alt 2. Dez 2004, 14:30
Der Garbage Collector von .NET

Der GC von .NET arbeitet als Standard immer in einem Hintergrund-Thread. Setzt man auf den GC, so hat man keinen direkten Einfluß auf den Zeitpunkt, zu welchem ein Objekt freigegeben wird.

Nachdem die letzte Referenz eines Objektes gelöscht wurde, kann die GC sich um die Entsorgung kümmern. Nochmal zu beachten gilt: Es macht unter .NET keinen Unterschied, ob wir jetzt über Referenzen via Object-Variablen oder Interface-Variablen oder beiden sprechen. Die GC kümmert sich jedoch nicht umgehend um dieses Objekt, d.h. dass der Speicher vorläufig nicht freigegeben wird. Wann sie der GC um da löschen kümmert, das kann man in der Regel nicht beeinflussen und muss man auch nicht. Der GC wird automatisch unter verschiedenen Bedingungen ausgeführt. Die wichtigste hierbei ist: der Speicher wird knapp. Wenn .NET der Meinung ist, es ist soweit, dann wird der GC gestartet. Dieser überprüft dann alle Referenzen auf Objekte und markiert die, auf welche nicht mehr verwiesen wird zur Freigabe und gibt diese ggf. auch sofort frei. Es kann jedoch auch mehrere Läufe des GC in Anspruch nehmen, bis ein Objekt freigegeben wird. Auch dieses ist nicht Thema dieses Tutorial.
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#5
  Alt 2. Dez 2004, 16:12
Destroy, Finalize und IDisposable

In Delphi für Win32 hat man immer die Methode Destroy überschrieben, wenn man Speicher und andere Resourcen (z.B. Datei-Handles) wieder freigeben musste. Diese wurde dann entweder direkt, indirekt über Free, oder bei Interfaces durch den Referenz-Mechanismus aufgerufen.

Destroy
Auch unter .NET kann man Destroy nutzen, allerdings darf man sich dann nicht mehr auf den GC von .NET verlassen, da dieser Destroy nicht aufrufen würde. Das heißt, wird Destroy nicht expliziet aufgerufen, so werden die darin enthaltenen nicht-verwalteten Resourcen (Dateihandles, Bitmaps, etc.) nicht freigegeben und es entsteht ein Speicherloch. Von daher sollte man unter .NET auf den Destructor Destroy ganz verzichten.

Finalize
Finalize wird vom GC aufgerufen, wenn ein Objekt als "nicht mehr refernziert" erkannt wurde. Das heisst, dass man reservierte nicht verwaltete Resourcen hier freigeben sollte.

Finalize hat jedoch einen gravierenden Nachteil: man kann nicht beeinflussen wann es durch den GC aufgerufen wird. Wenn eine Klasse jetzt zum Beispiel eine Datei exklusiv öffnet, kann kein anderer Process (oder der gleiche) noch mal auf diese Datei zugreifen, bis der GC Finalize aufgerufen hat. Und noch einmal: wann dieses geschieht, dass kann man nicht beeinflussen!

IDisposable
Das Dispose Pattern beschreibt wie ein Objekt seine Resourcen freigeben kann und somit auch, wie der Nutzer des Objektes dieses einsetzen sollte. Mit IDisposable wird ein Mechanismus zum freigeben der reservierten Resourcen definiert. IDisposable definiert nur eine Methode Dispose();. Diese wird durch das implementierende Objekt aufgerufen, wenn es das implementierte Objekt (also jenes, das die Resourcen blockiert) nicht mehr braucht. In der Implementierung Dispose(); werden dann alle Resourcen wieder freigegeben.
Hinweis: Nach dem Aufrufen von Dispose(); wird der vom Objekt belegte Speicher selbst nicht sofort freigegeben, das übernimmt der GC zu einem späteren Zeitpunkt.

Wann nutzt man Finalize, wann nicht
Nutzt ein Objekt lediglich andere verwaltete Resourcen (also i.A. andere Objekte und Standardtypen wie Integer, String, etc.) so sollte man Finalize nicht implementieren, da das die Freigabe des reservierten Speicher nur auf mind. 2 GC-Aufrufe verzögert. Alle verwalteten Objekte gibt die GC automatisch selbständig frei, wenn diese nicht mehr gebraucht werden.
Finalize sollte man nur nutzen, wenn man nicht verwaltete Resourcen (z.B. Dateihandle, Bitmaps, etc.) freigeben muss.

Braucht man Finalize wenn man IDisposable nutzt
Einfache Antwort: Ja. IDisposable dient dem Programmierer, der das Objekt einsetzt dazu festzulegen, wenn ein Objekt seine Resourcen freigeben soll. Wenn der Programmierer dieses dann auch immer tut, dann ist Finalize oft unnötig. Aber wer kann garantieren, dass der Nutzer des Objektes daran auch denkt? Keiner, also kann es zu Speicherlöschern und ähnlichen Problemen kommen. Deshalb sollte man dann auch immer Finalize implementieren. Hier wiederum muss man aufpassen, dass man jetzt evtl. freigegebene Resourcen nicht nochmal versucht freizugeben. Das jedoch hebe ich mir für ein weiteres Tutorial auf.
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#6
  Alt 2. Dez 2004, 16:54
Ein kleines Demo Programm

Anbei ist ein kleines Demo-Programm, welches die Arbeitsweise der GC demonstriert. Es ist in zwei Units aufgeteilt.

Unit: uSampleObject.pas

In dieser Unit wird ein Interface deklariert, welches nur eine einfache Zugriffsmethode hat, die uns die ID des zugehörigen Objektes zurückliefert.
Delphi-Quellcode:
type
  IMyInterface = interface
    function GetMyNumber: Integer;
  end;
Auch ist das implementierende Objekt in der Unit deklariert. Hier habe ich gleichzeitig noch ein kleines .NET Feature genutzt: Klassenkonstruktoren und Klassenvariablen. Dazu schreibe ich hier nicht viel weiter.
Hinweis:
Die Klassenmethode GetNextId (Achtung, nicht thread-safe) erhöht intern einen Klassenvariable (FIdCounter) und liefert den neuen Wert zurück. Damit kann jedes Objekt eine eindeutige ID zurückliefern. Dieses habe ich ausschließlich genutzt, damit im Debug-Log verfolgt werden kann, auf welches Objekt jeweils zugegriffen wird.

Hier die wichtigen Teile der Klassendefinition:
Delphi-Quellcode:
type
  MyInterfacedObject = class(System.Object, IMyInterface)
  strict private
    FMyNumber: Integer;
    procedure LogToDebugView(aLine: String);
  strict protected
    procedure Finalize; override;
  public
    constructor Create();
    function GetMyNumber: Integer;
  end;
Der Rest kann dem angehängtem Beispiel entnommen werden.

Das WinForm: uFormMain.pas

Ich habe das Form in drei Bereiche aufgeteilt. Abschnitt [1] zeigt den Zusammenhang zwischen Objekt-Referenzen und Interface-Referenzen. Abschnitt [2] ermöglicht das schnellere Füllen des Speichers, um das Verhalten der GC einfacher zu beurteilen. Abschnitt [3] dient dem Management der GC.

Alle Aktionen (Objekt erstellen und zerstören, Abfrage der Objekt-ID) werden im Ereignisprotokoll in Delphi 2005 mitgeschrieben und angezeigt.

Test 1
Starte das Programm (neu). Erstelle ein Objekt. (1) Lasse Dir die Objekt-Nummer anzeigen (2). Weise es dem Interface zu (4). Jetzt haben wir zwei Referenzen auf das gleiche Objekt. Lasse Dir die Interface-Nummer anzeigen (5). Diese sollte jetzt gleich der im Objekt sein. Lösche das Interface. (6) Starte den GC. (11) Im Log sollte jetzt nicht erscheinen, dass Objekte freigeben werden, da das Objekt immer noch durch die Objektvariable (1) angesprochen werden kann. In Delphi für Win32 wäre die jetzt ungültig Jetzt gib das Objekt frei (3). Weiterhin taucht im Debugger-Log sehr wahrscheinlich keine Message über die Freigabe auf. Wenn Du jetzt einen Tag wartest, wird diese wahrscheinlich irgendwann man auftauchen. Nun zwingen wir nochmal die GC aufzuräumen (11) Jetzt erscheint auch die Meldung, dass das Objekt freigegeben wurde.

Test 2
Starte das Programm (neu). Erstelle einmal 10 Objekte. (7) Beachte, dass keines dieser Objekte jetzt mehr referenziert wird. Allerdings befinden sich alle Objekte weiterhin im Speicher, der GC hat diese sehr wahrscheinlich noch nicht freigegeben. Jetzt wieder auf (11) und alle Objekte werden freigegeben.

Test 3
Starte das Programm (neu). Erstelle mehrmals mehrere Objekte (7). Nach einer Weile wirst Du im Debug-Log bemerken, dass die GC anspringt und die Objekte selbständig freigibt. Das System hat automatisch den Bedarf an Speicher bemerkt und angefangen einige Objekte aufzuräumen. Das ist das beste einfache Beispiel, um zu sehen dass und wie die GC arbeitet.
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#7
  Alt 2. Dez 2004, 16:56
Für den Anfang soll dieses Tutorial nur einen kurzen Einblick in die Arbeit der Garbage Collection von .NET bieten. Es ist auf keinen Fall allumfassend und an einigen Stellen habe ich absichtlich einiges sehr vereinfacht, da die gesamte GC so komplex ist, dass mehrere Tutorials angesagt sind.

Wer dennoch gravierende Fehler findet, melde sich bitte via PM bei mir, ich nehme mich dessen dann zu gegebener Zeit an

......

Stay tuned for more in the Internals of Delphi and .NET
Angehängte Dateien
Dateityp: zip garbage_collector_i.zip (21,5 KB, 17x aufgerufen)
Daniel W.
 
Tutorial geschlossen


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 05:28 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz