Laden...

Wie sieht das (physische) Speicherlayout eines Objekts (auf dem Heap) aus?

Erstellt von ManuMerhad vor 11 Jahren Letzter Beitrag vor 11 Jahren 4.314 Views
M
ManuMerhad Themenstarter:in
4 Beiträge seit 2012
vor 11 Jahren
Wie sieht das (physische) Speicherlayout eines Objekts (auf dem Heap) aus?

Sehr geehrte Damen und Herren,

ich bräuchte einen Ratschlag zum Thema Objekterzeugung im Heap.

Es geht darum, dass ich gerne wisssen würde, ob ich mit meiner Annahme richtig liege, dass die Größe (Speicherbedarf) eines Objektes im Heap daraus berechnet wird, dass zunächt die ersten vier Byte des Objektes für die Adresse reserviert wird auf die die dazugehörige Referenz zeigt, wobei diese dann selbst auf die MethodTable zeigt (virtual und override). Zeigt die Adresse immer auf die Method Table auch wenn sie keine virtuellen Methoden enthält?
Nach diesen vier Byte folgt wiederum ein Zeiger mit vier Byte auf den syncblock. Ist das so richtig, ist der syncblock immer vorhanden?
Was machen eigentlich Syncblock und Method Table?
Nach diesen 8 Byte müssten die Felder des Objekts ihre Werte erhalten. Wird der Speicherbedarf einfach so 8 Byte plus Felder ausgerechnet? Gibt es noch zusätzlichen Bedarf für Verwaltungsinformationen, welche sind das? Was ist eigentlich mit den Methoden des Objektes, die werden ja im Codesegment gespeichert, wie ist die Verbindung zum Objekt im Heap. Werden auch irgendwelche Informationen oder Daten zu Methoden im Heap gespeichert?

Vielen Dank für Informationen!

MfG

Merhard

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo ManuMerhad,

Es geht darum, dass ich gerne wisssen würde, ob ich mit meiner Annahme richtig liege, dass die Größe (Speicherbedarf) eines Objektes im Heap daraus berechnet wird, dass zunächt die ersten vier Byte des Objektes für die Adresse reserviert wird auf die die dazugehörige Referenz zeigt, wobei diese dann selbst auf die MethodTable zeigt (virtual und override).

nach dem, was ich gelesen habe, kommt zuerst ein sync block index, dann ein Zeiger auf die MethodTable und dann die eigentlichen Objektdaten. Wieviel Bytes die Zusatzinformationen einnehmen, weiß ich nicht. Möglicherweise hängt das von der Bittigkeit der Anwendung ab. Auch ist nicht ganz klar, ob die Referenz (auf das Objekt) auf den sync block index oder den Zeiger auf die Method Table zeigt. Bei einigen Bildern zeigt sie auf den Zeiger auf die Method Table, keine Ahnung, ob das Absicht oder Versehen ist.

Zeigt die Adresse immer auf die Method Table auch wenn sie keine virtuellen Methoden enthält?

Die Frage stellt sich nicht, weil alle Objekte virtuelle Methoden haben, man denke nur an ToString. Es gibt also immer einen Zeiger auf die Method Table.

Nach diesen vier Byte folgt wiederum ein Zeiger mit vier Byte auf den syncblock. Ist das so richtig, ist der syncblock immer vorhanden?

Wie schon gesagt, kommt der sync block index wohl vor dem Zeiger auf die Method Table. Und der syncblock ist wohl immer vorhanden. Und auf exakt vier Bytes würde ich mich insbesondere beim Zeiger auf die Method Table insbesondere unter 64bit nicht wirklich festlegen.

Was machen eigentlich Syncblock und Method Table?

Das sollte sich aus den Namen bzw. dem hier gesagten ergeben. Wenn nicht gibt es ja noch Google & Co.

Nach diesen 8 Byte müssten die Felder des Objekts ihre Werte erhalten. Wird der Speicherbedarf einfach so 8 Byte plus Felder ausgerechnet?

Grundsätzlich: Platz für sync block index plus Platz Method Table plus Platz für die eigentlichen Objektdaten.

Gibt es noch zusätzlichen Bedarf für Verwaltungsinformationen, welche sind das?

Alle zusätzlichen Informationen stehen in der Method Table selbst, die ja mehr enthält als nur die Pointer auf die Methoden, sondern auch weitere Informationen zur Klasse und Oberklasse.

Was ist eigentlich mit den Methoden des Objektes, die werden ja im Codesegment gespeichert,

korrekt

wie ist die Verbindung zum Objekt im Heap.

Eben über die Method Table.

Werden auch irgendwelche Informationen oder Daten zu Methoden im Heap gespeichert?

Nein, außer dem Zeiger auf die Method Table.

Bleibt nur die Frage, wofür du das wissen willst, denn diese konkrete physische Sicht braucht man beim normalen Programmieren überhaupt nicht. Und ein direkter physischer Zugriff auf verwaltete Objekte ist keine gute Idee.

herbivore

M
ManuMerhad Themenstarter:in
4 Beiträge seit 2012
vor 11 Jahren

Vielen Dank für die Antwort.

Der Grund warum ich das so genau wissen will ist, dass ich das für mein Studium wissen muss.

Nun noch ein paar Fragen die sich daraus ergeben:
Ist die Virtual Method Table demnach gleichbedeutend mit der Method Table für jedes Objekt?
Die Method Table (bzw. Virtual Methode Table), sind die in einem eigenen Segment im Speicher oder sind die logischerweise im Codesegment? (p.s. habe gehört Vtabble wäre ein eigenes Speichersegment). Oder, was auch sinnvoll wäre, ist die Method Table eine Art Datenregister im CPU?
So müsste sich der Rückgabewert der Methode einer Klasse ja auch im Datenregister der CPU sein, da er ja generell nie im Heap oder Stack vorkommt, weshalb ja auch kein Speicher im Objekt benötigt wird, oder?
Ist der Assembly-Code eigentlich gleichbedeutend mit dem Code im Codesegment?

Gruß

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo ManuMerhad,

Ist die Virtual Method Table demnach gleichbedeutend mit der Method Table für jedes Objekt?

das "Virtual" wird nur der Kürze wegen manchmal weggelassen, beides meint aber das gleiche. Allerdings gibt es die (Virtual) Method Table nur einmal pro Klasse und alle Objekte der Klasse verweisen dann darauf.

Die Method Table (bzw. Virtual Methode Table), sind die in einem eigenen Segment im Speicher oder sind die logischerweise im Codesegment? (p.s. habe gehört Vtabble wäre ein eigenes Speichersegment).

Die VTM liegt im Datensegment. Im Codesegment wäre Quatsch, denn sie enthält ja nur Daten (z.B. Zeiger auf Methoden), keinen ausführbaren Code. Ob in einem separaten Bereich im Datensegment bzw. wo genau im Datensegment, weiß ich nicht und spielt auch keine wirkliche Rolle.

Oder, was auch sinnvoll wäre, ist die Method Table eine Art Datenregister im CPU?

Natürlich werden die Daten aus der VTM bei Bedarf in die CPU-Register geladen, sind aber nicht dauerhaft dort enthalten.

So müsste sich der Rückgabewert der Methode einer Klasse ja auch im Datenregister der CPU sein, da er ja generell nie im Heap oder Stack vorkommt, weshalb ja auch kein Speicher im Objekt benötigt wird, oder?

Der Rückgabewert einer Methode kann per Register übergeben werden; bei Referenzentypen oder umfangreichen Werttypen, die nicht direkt ins Register passen, natürlich nur eine Referenz bzw. ein Pointer auf die eigentlichen Daten. Der Rückgabewert (bzw. eben die Referenz oder der Pointer darauf) kann aber auch per Stack übergeben werden. Genauso wie Parameter (bzw. deren Referenzen) auf dem Stack übergeben werden.

Ist der Assembly-Code eigentlich gleichbedeutend mit dem Code im Codesegment?

Das Codesegment ist der Speicherbereich, der den Maschinencode aufnimmt.

Wenn du das alles fürs Studium brauchst, solltest du allerdings versuchen, dir nicht nur - wie du es wohl getan hast - möglichst viele Informationen selber zu beschaffen, sondern auch durch den Vergleich mehrerer Quellen solche und ähnliche Fragen selbst zu klären.

herbivore

1.361 Beiträge seit 2007
vor 11 Jahren

Hi ManuMerhad,

ich hatte mal auf eine ähnlich geartete Frage geantwortet und auch dort ein paar Quellen verlinkt. Siehe Speicherbedarf von Objekten ermitteln/berechnen.

Darüberhinaus würde ich persönlich nicht von CodeSegment sprechen, da dies sich mehr auf das Layout von nativen Executable/Object-Files (unter Windows PE/COFF) bezieht. Dort hat man eine Segmentierung.
.NET Executables sind Erweiterungen vom normalen PE-Format, die eine zusätzliche Sektion einführen. Der IL-Code liegt also schonmal nicht im Codesegment, sondern in der CLR Data section.

Und auch der vom JITter übersetzte Maschinencode landet einfach im Hauptspeicher, der nichts mit den Segmenten des Object-Files zu tun hat.

beste Grüße
zommi

M
ManuMerhad Themenstarter:in
4 Beiträge seit 2012
vor 11 Jahren

Viele Dank für die Informationen!
Sie haben mir sehr weitergeholfen.

Es fehlt eigentlich nur eine Sache. Ein String ist ja auch ein Referenztyp. Der setzt sich aus mindestestens 20 Byte zusammen. Der Rest ist für die Zeichen. (Heap). Wie setzen sich diese 20 Byte zusammen?

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo ManuMerhad,

wenn du schon weißt, dass es 20 Bytes sind, dann wirst du auch noch den Aufbau selbst herausbekommen. 😃

Ich bin bisher allerdings davon ausgegangen, dass die eigentliche (Zeichen-)Information in einem char-Array steht und ein (char-)Array hat eigentlich nur die Längenangabe (Length) als Overhead (plus natürlich des Overheads, den jedes Objekt hat, wie oben besprochen). Vielleicht kommt auch noch eine Angabe zur Dimensionalität hinzu, aber das habe ich nicht parat.

herbivore

1.346 Beiträge seit 2008
vor 11 Jahren

Allerdings weden string Konstanten gecached und mehrfach verwendet, falls das für dich auch noch wichtig ist.


            string str = "myCSharp.de";
            string str2 = "myCSharp";
            string str3 = ".de";
            string str4 = str2 + str3;
            bool b = object.ReferenceEquals(str, str4); // false -> neues Objekt
            string str5 = "myCSharp.de";
            bool b2 = object.ReferenceEquals(str, str5); // true -> gleiches Objekt, also ist str5 nur 4(8) bytes overhead

M
ManuMerhad Themenstarter:in
4 Beiträge seit 2012
vor 11 Jahren

ich nehm an, dass bezeichnet die Länge des Strings:

The sync block, used for locking the hash code (or a thin lock - see Brian's comment)  
Type pointer  
Size of array  
Element type pointer  
Null reference (first element)
1.361 Beiträge seit 2007
vor 11 Jahren

Hi ManuMerhad,

du hast Recht, dass Strings nochmal eine Sonderrolle und damit ein anderes Speicherlayout in der .NET CLR besitzen. Hierauf gehen die Artikel Stackoverflow: Where does .NET place the String value? und einige der Blogposts von NetInverse Developers Blog [clr] ein.

In der CLR sieht ein String unter x86 wie folgt aus:


[SyncBlock] 4 Bytes
[MethodTablePointer] 4 Bytes
 [color][ArrayLength] 4 Bytes (nur in der 2.0er CLR, fiel ab 4.0 weg)[/COLOR]
[StringLength] 4 Bytes
[Characters...] 2*n Bytes << Die eigentlichen Zeichen mit je 2 Bytes (in UCS-2 Kodierung)
[TerminatingNull] 2 Bytes

und ähnelt damit dem Speicherlayout von BSTR in der nativen OLE/COM Welt.

Insgesamt ergibt sich also so für 4.0 ein Overhead von 14 Bytes (4+4+4+2). In der 2.0er CLR entsprechend 18 Bytes.

Ein String mit dem 4-Zeichen langen Wort "Test" belegt also in .NET 4.0 insgesamt 22 Bytes (14 + 2*4). Und das können wir uns natürlich auch direkt im Speicher anschauen. Hierfür nutzen wir die SOS (Son of Strike) Extensions zum Debugging von .NET Applikationen. In einer VisualStudio Debug-Session kann man die komfortabel aus dem ImmediateWindow heraus nutzen. (The Immediate Window: Running WinDbg and SOS (Son of Strike) Commands)

Ich habe das mal mit einem winzigen Testprogramm gemacht und das Debug-Ergebnis bestätigt die 22 Bytes von "Test" und ist im Anhang zu sehen.

beste Grüße
zommi

1.361 Beiträge seit 2007
vor 11 Jahren

Hier noch ein Nachtrag:

Ich hatte mich gefragt, warum der SyncBlock von dem String aus meiner DebugSession nicht 0, sondern 0x80000000 war. (siehe mein Screenshot im VS-Fenster "Memory1"). 🤔

Und da hilft nur noch das Lesen im Quellcode von Microsofts SSCLI (Shared Source Common Language Infrastructure), die man hier herunterladen kann. Zwar ist die SSCLI erst recht nicht mehr aktuell, aber soll zumindest der offiziellen 2.0er CLR sehr ähnlich sein. Interessant sind in diesem Kontext die folgenden Dateien:


clr/src/vm/object.h
clr/src/vm/syncblk.h

Man kann hier nochmal den generellen Aufbau von GC-Objekten nachlesen, sowie welche Spezialtypen es neben "normalen" Objekten noch gibt, wie eben Strings oder die zahlreichen Array-Varianten.
Im Grunde lassen sich alle Fragen von ManuMerhad durch das Studieren der object.h beantworten. (In der 2.0er Welt)

Aber zurück zum nicht genullten SyncBlock meines Strings. Die syncblk.h verrät mir:

// m_SyncBlockValue is carved up into an index and a set of bits.  Steal bits by
// reducing the mask.  We use the very high bit, in _DEBUG, to be sure we never forget
// to mask the Value to obtain the Index

    // These first three are only used on strings (If the first one is on, we know whether 
    // the string has high byte characters, and the second bit tells which way it is. 
    // Note that we are reusing the FINALIZER_RUN bit since strings don't have finalizers,
    // so the value of this bit does not matter for strings
#define BIT_SBLK_STRING_HAS_NO_HIGH_CHARS   0x80000000

Vereinfacht gesagt: Enthält mein String nur ASCII-Zeichen, dann ist im SyncBlock im Debug-Mode das höchste Bit gesetzt. Und siehe da, im Release-Mode verschwindet das Bit auch wieder! Und auch, wenn ich ein "high byte character" wie das '€'-Zeichen im String verwende (entspricht U+20AC). Also stimmt dieser Teil zumindest auch noch in der .NET 4.0 Welt 🙂

beste Grüße
zommi

1.361 Beiträge seit 2007
vor 11 Jahren

Nochmals hi,

vielleicht haben sich einige von euch auch gefragt:

warum der String null-terminiert ist, obwohl doch

a) dessen Länge bekannt ist
b) der Wert 0x0000 wohl auch innerhalb eines Strings vorkommen kann?

Meinen Recherchen nach ist das Length-Field vor dem eigentlichen String zur Performancesteigerung da. Man muss nicht immer bis zum '\0' scannen. Andererseits arbeitet die WinAPI intern nur mit normalen nullterminierten Strings, LPWStr (A pointer to a null-terminated array of Unicode characters).

Und nun ist es praktisch, dass die .NET Strings trotzdem null-terminiert sind, da man sie ohne zusätzlichen Aufwand direkt an eine WinAPI-Funktion durchreichen kann. Und das ist auch das default Marshalling für Strings bei PInvoke. Allerdings muss man dann aufpassen, dass kein '\0' drin vorkommt.

Der folgende Code veranschaulicht das:


        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

        static void Main(string[] args)
        {
            string hallo = "Hallo\0 Welt";
            System.Console.WriteLine(hallo);  // gibt alles aus
            MessageBox(IntPtr.Zero, hallo, "", 0); // gibt nur 'Hallo' aus
            System.Windows.Forms.MessageBox.Show(hallo); // gibt nur 'Hallo' aus
        }

Die Implementierung der Strings in .NET sollte also sowohl WinApi-kompatibel als auch performant sein.

beste Grüße
zommi