Sizeof für Java

26. Dezember 2003

F: Hat Java einen Operator wie sizeof () in C?

A: Eine oberflächliche Antwort ist, dass Java nichts wie C bietet sizeof(). Lassen Sie uns jedoch überlegen, warum ein Java-Programmierer dies gelegentlich wünscht.

Der AC-Programmierer verwaltet die meisten Speicherzuordnungen sizeof()für die Datenstruktur selbst und ist für die Kenntnis der zuzuordnenden Speicherblockgrößen unverzichtbar. Darüber hinaus malloc()tun C-Speicherzuweiser bei der Objektinitialisierung fast nichts: Ein Programmierer muss alle Objektfelder festlegen, die Zeiger auf weitere Objekte sind. Aber wenn alles gesagt und codiert ist, ist die C / C ++ - Speicherzuweisung ziemlich effizient.

Im Vergleich dazu sind Java-Objektzuweisung und -Konstruktion miteinander verbunden (es ist unmöglich, eine zugewiesene, aber nicht initialisierte Objektinstanz zu verwenden). Wenn eine Java-Klasse Felder definiert, die auf weitere Objekte verweisen, werden diese häufig auch zur Erstellungszeit festgelegt. Durch das Zuweisen eines Java-Objekts werden daher häufig zahlreiche miteinander verbundene Objektinstanzen zugewiesen: ein Objektdiagramm. In Verbindung mit der automatischen Speicherbereinigung ist dies allzu praktisch und kann dazu führen, dass Sie sich nie um Details zur Java-Speicherzuweisung kümmern müssen.

Dies funktioniert natürlich nur für einfache Java-Anwendungen. Im Vergleich zu C / C ++ belegen äquivalente Java-Datenstrukturen tendenziell mehr physischen Speicher. In der Entwicklung von Unternehmenssoftware ist die Annäherung an den maximal verfügbaren virtuellen Speicher der heutigen 32-Bit-JVMs eine häufige Einschränkung der Skalierbarkeit. Ein Java-Programmierer könnte also davon profitieren sizeof()oder etwas Ähnliches, um zu beobachten, ob seine Datenstrukturen zu groß werden oder Speicherengpässe enthalten. Glücklicherweise können Sie mit Java Reflection ein solches Tool ganz einfach schreiben.

Bevor ich fortfahre, werde ich auf einige häufige, aber falsche Antworten auf die Frage dieses Artikels verzichten.

Irrtum: Sizeof () wird nicht benötigt, da die Größen der Java-Basistypen festgelegt sind

Ja, ein Java intist 32 Bit in allen JVMs und auf allen Plattformen, aber dies ist nur eine Sprachspezifikationsanforderung für die vom Programmierer wahrnehmbare Breite dieses Datentyps. Dies intist im Wesentlichen ein abstrakter Datentyp und kann beispielsweise durch ein physisches 64-Bit-Speicherwort auf einer 64-Bit-Maschine gesichert werden. Gleiches gilt für nicht primitive Typen: Die Java-Sprachspezifikation sagt nichts darüber aus, wie Klassenfelder im physischen Speicher ausgerichtet werden sollen oder dass ein Array von Booleschen Werten nicht als kompakter Bitvektor in der JVM implementiert werden kann.

Irrtum: Sie können die Größe eines Objekts messen, indem Sie es in einen Byte-Stream serialisieren und die resultierende Stream-Länge anzeigen

Der Grund, warum dies nicht funktioniert, ist, dass das Serialisierungslayout nur eine Fernreflexion des tatsächlichen speicherinternen Layouts ist. Eine einfache Möglichkeit, dies zu erkennen, besteht darin, zu untersuchen, wie Strings serialisiert werden: Im Speicher sind alle charmindestens 2 Byte Stringgroß , in serialisierter Form sind sie jedoch UTF-8-codiert, sodass jeder ASCII-Inhalt halb so viel Platz beansprucht.

Ein weiterer Arbeitsansatz

Sie erinnern sich vielleicht an "Java-Tipp 130: Kennen Sie Ihre Datengröße?" Dies beschrieb eine Technik, die auf der Erstellung einer großen Anzahl identischer Klasseninstanzen und der sorgfältigen Messung der resultierenden Zunahme der verwendeten JVM-Heap-Größe basiert. Wenn zutreffend, funktioniert diese Idee sehr gut, und ich werde sie tatsächlich verwenden, um den alternativen Ansatz in diesem Artikel zu booten.

Beachten Sie, dass die SizeofKlasse von Java Tip 130 eine ruhende JVM erfordert (sodass die Heap-Aktivität nur auf Objektzuweisungen und Garbage Collections zurückzuführen ist, die vom Mess-Thread angefordert werden) und eine große Anzahl identischer Objektinstanzen erfordert. Dies funktioniert nicht, wenn Sie die Größe eines einzelnen großen Objekts (möglicherweise als Teil einer Debug-Trace-Ausgabe) ändern möchten und insbesondere, wenn Sie untersuchen möchten, was es tatsächlich so groß gemacht hat.

Wie groß ist ein Objekt?

Die obige Diskussion hebt einen philosophischen Punkt hervor: Was ist die Definition einer Objektgröße, da Sie sich normalerweise mit Objektgraphen befassen? Ist es nur die Größe der Objektinstanz, die Sie untersuchen, oder die Größe des gesamten Datengraphen, der auf der Objektinstanz verwurzelt ist? Letzteres ist in der Praxis normalerweise wichtiger. Wie Sie sehen werden, sind die Dinge nicht immer so klar, aber für den Anfang können Sie diesen Ansatz verfolgen:

  • Eine Objektinstanz kann (ungefähr) dimensioniert werden, indem alle nicht statischen Datenfelder (einschließlich der in Oberklassen definierten Felder) summiert werden.
  • Im Gegensatz zu beispielsweise C ++ haben Klassenmethoden und ihre Virtualität keinen Einfluss auf die Objektgröße
  • Klassen-Superschnittstellen haben keinen Einfluss auf die Objektgröße (siehe Hinweis am Ende dieser Liste).
  • Die volle Objektgröße kann als Abschluss über den gesamten am Startobjekt verwurzelten Objektgraphen erhalten werden
Hinweis: Die Implementierung einer Java-Schnittstelle markiert lediglich die betreffende Klasse und fügt ihrer Definition keine Daten hinzu. Tatsächlich überprüft die JVM nicht einmal, dass eine Schnittstellenimplementierung alle für die Schnittstelle erforderlichen Methoden bereitstellt. Dies liegt in den aktuellen Spezifikationen ausschließlich in der Verantwortung des Compilers.

Um den Prozess zu booten, verwende ich für primitive Datentypen physikalische Größen, die von der SizeofKlasse von Java Tip 130 gemessen werden . Wie sich herausstellt, java.lang.Objectnimmt eine Ebene für gängige 32-Bit-JVMs 8 Byte ein, und die Basisdatentypen haben normalerweise die geringste physische Größe, die den Sprachanforderungen gerecht wird (außer booleanein ganzes Byte):

// java.lang.Object Shell-Größe in Bytes: public static final int OBJECT_SHELL_SIZE = 8; public static final int OBJREF_SIZE = 4; public static final int LONG_FIELD_SIZE = 8; public static final int INT_FIELD_SIZE = 4; public static final int SHORT_FIELD_SIZE = 2; public static final int CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; public static final int BOOLEAN_FIELD_SIZE = 1; public static final int DOUBLE_FIELD_SIZE = 8; public static final int FLOAT_FIELD_SIZE = 4;

(Es ist wichtig zu wissen, dass diese Konstanten nicht für immer fest codiert sind und für eine bestimmte JVM unabhängig gemessen werden müssen.) Natürlich vernachlässigt die naive Summe der Objektfeldgrößen Speicherausrichtungsprobleme in der JVM. Die Speicherausrichtung spielt eine Rolle (wie zum Beispiel für primitive Array-Typen in Java Tip 130 gezeigt), aber ich denke, es ist unrentabel, solchen Details auf niedriger Ebene nachzujagen. Solche Details hängen nicht nur vom JVM-Anbieter ab, sie unterliegen auch nicht der Kontrolle des Programmierers. Unser Ziel ist es, eine gute Schätzung der Objektgröße zu erhalten und hoffentlich einen Hinweis darauf zu erhalten, wann ein Klassenfeld redundant sein könnte. oder wenn ein Feld träge besiedelt sein sollte; oder wenn eine kompaktere verschachtelte Datenstruktur erforderlich ist usw. Für absolute physische Präzision können Sie jederzeit zur SizeofKlasse in Java Tip 130 zurückkehren.

Um das Profil einer Objektinstanz zu ermitteln, berechnet unser Tool nicht nur die Größe, sondern erstellt auch eine hilfreiche Datenstruktur als Nebenprodukt: ein Diagramm aus IObjectProfileNodes:

Schnittstelle IObjectProfileNode {Object object (); String name (); int size (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] children (); IObjectProfileNode shell (); IObjectProfileNode [] path (); IObjectProfileNode root (); int pathlength (); Boolesche Traverse (INodeFilter-Filter, INodeVisitor-Besucher); String dump (); } // Ende der Schnittstelle

IObjectProfileNodes sind fast genauso miteinander verbunden wie der ursprüngliche Objektgraph, wobei IObjectProfileNode.object()das reale Objekt zurückgegeben wird, das jeder Knoten darstellt. IObjectProfileNode.size()Gibt die Gesamtgröße (in Byte) des Objektunterbaums zurück, der auf der Objektinstanz dieses Knotens verwurzelt ist. Wenn eine Objektinstanz über Instanzfelder ungleich Null oder über in Arrayfeldern enthaltene Referenzen mit anderen Objekten verknüpft ist, IObjectProfileNode.children()wird eine entsprechende Liste von untergeordneten Diagrammknoten in absteigender Größenreihenfolge sortiert. Umgekehrt gibt für jeden anderen Knoten als den Startknoten der IObjectProfileNode.parent()übergeordnete Knoten zurück. Die gesamte Sammlung von IObjectProfileNodes schneidet und würfelt somit das ursprüngliche Objekt und zeigt, wie der Datenspeicher darin aufgeteilt ist. Darüber hinaus werden die Namen der Diagrammknoten aus den Klassenfeldern abgeleitet und der Pfad eines Knotens innerhalb des Diagramms untersucht (IObjectProfileNode.path()) ermöglicht es Ihnen, die Eigentumsverknüpfungen von der ursprünglichen Objektinstanz zu internen Daten zu verfolgen.

Möglicherweise haben Sie beim Lesen des vorherigen Absatzes bemerkt, dass die Idee bisher noch nicht eindeutig ist. Wenn Sie beim Durchlaufen des Objektdiagramms mehrmals auf dieselbe Objektinstanz stoßen (dh mehr als ein Feld irgendwo im Diagramm darauf zeigt), wie weisen Sie dessen Besitz zu (den übergeordneten Zeiger)? Betrachten Sie dieses Code-Snippet:

 Objekt obj = neuer String [] {neuer String ("JavaWorld"), neuer String ("JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

Das Nachdenken über Graphenüberquerungen und kürzeste Pfade sollte an dieser Stelle eine Glocke läuten: Die Breitensuche ist ein Algorithmus zum Durchlaufen von Graphen, der garantiert, dass der kürzeste Weg vom Startknoten zu jedem anderen erreichbaren Graphenknoten gefunden wird.

Nach all diesen Vorbereitungen finden Sie hier eine Lehrbuchimplementierung einer solchen Graphenüberquerung. (Einige Details und Hilfsmethoden wurden weggelassen. Ausführliche Informationen finden Sie im Download dieses Artikels.):