Sizeof per Java

26 dicembre 2003

D: Java ha un operatore come sizeof () in C?

A: Una risposta superficiale è che Java non prevede nulla di simile C di sizeof(). Tuttavia, consideriamo perché un programmatore Java potrebbe occasionalmente volerlo.

Il programmatore AC gestisce da solo la maggior parte delle allocazioni di memoria della struttura dati ed sizeof()è indispensabile per conoscere le dimensioni dei blocchi di memoria da allocare. Inoltre, gli allocatori di memoria C malloc()non fanno quasi nulla per quanto riguarda l'inizializzazione degli oggetti: un programmatore deve impostare tutti i campi oggetto che sono puntatori a ulteriori oggetti. Ma quando tutto è detto e codificato, l'allocazione della memoria C / C ++ è abbastanza efficiente.

In confronto, l'allocazione e la costruzione di oggetti Java sono legate insieme (è impossibile utilizzare un'istanza di oggetto allocata ma non inizializzata). Se una classe Java definisce campi che sono riferimenti ad ulteriori oggetti, è anche comune impostarli in fase di costruzione. Pertanto, l'assegnazione di un oggetto Java alloca frequentemente numerose istanze di oggetti interconnessi: un oggetto grafico. Insieme alla raccolta automatica dei rifiuti, questo è fin troppo conveniente e può farti sentire come se non dovessi mai preoccuparti dei dettagli di allocazione della memoria Java.

Ovviamente funziona solo per semplici applicazioni Java. Rispetto a C / C ++, le strutture dati Java equivalenti tendono a occupare più memoria fisica. Nello sviluppo di software aziendale, avvicinarsi alla massima memoria virtuale disponibile sulle odierne JVM a 32 bit è un vincolo di scalabilità comune. Pertanto, un programmatore Java potrebbe trarre vantaggio da sizeof()qualcosa di simile per tenere d'occhio se le sue strutture dati stanno diventando troppo grandi o contengono colli di bottiglia della memoria. Fortunatamente, la riflessione Java ti consente di scrivere uno strumento del genere abbastanza facilmente.

Prima di procedere, farò a meno di alcune risposte frequenti ma errate alla domanda di questo articolo.

Errore: Sizeof () non è necessario perché le dimensioni dei tipi di base Java sono fisse

Sì, Java intè a 32 bit in tutte le JVM e su tutte le piattaforme, ma questo è solo un requisito della specifica del linguaggio per la larghezza percepibile dal programmatore di questo tipo di dati. Tale intè essenzialmente un tipo di dati astratto e può essere supportato, ad esempio, da una parola di memoria fisica a 64 bit su una macchina a 64 bit. Lo stesso vale per i tipi non primitivi: la specifica del linguaggio Java non dice nulla su come i campi della classe dovrebbero essere allineati nella memoria fisica o che un array di booleani non può essere implementato come un bitvector compatto all'interno della JVM.

Errore: è possibile misurare la dimensione di un oggetto serializzandolo in un flusso di byte e osservando la lunghezza del flusso risultante

Il motivo per cui questo non funziona è perché il layout di serializzazione è solo un riflesso remoto del vero layout in memoria. Un modo semplice per vederlo è osservare come Stringvengono serializzati: in memoria ognuno charè di almeno 2 byte, ma nella forma serializzata i Strings sono codificati in UTF-8 e quindi qualsiasi contenuto ASCII occupa la metà dello spazio.

Un altro approccio lavorativo

Potresti ricordare "Suggerimento Java 130: conosci la dimensione dei tuoi dati?" che descriveva una tecnica basata sulla creazione di un gran numero di istanze di classe identiche e sulla misurazione accurata dell'aumento risultante nella dimensione dell'heap utilizzata dalla JVM. Quando applicabile, questa idea funziona molto bene e la userò in effetti per avviare l'approccio alternativo in questo articolo.

Notare che la Sizeofclasse di Java Tip 130 richiede una JVM quiescente (in modo che l'attività di heap sia dovuta solo alle allocazioni di oggetti e alle raccolte di dati inutili richieste dal thread di misurazione) e richiede un numero elevato di istanze di oggetti identiche. Ciò non funziona quando si desidera ridimensionare un singolo oggetto di grandi dimensioni (magari come parte dell'output di una traccia di debug) e soprattutto quando si desidera esaminare cosa lo ha effettivamente reso così grande.

Qual è la dimensione di un oggetto?

La discussione sopra evidenzia un punto filosofico: dato che di solito ti occupi di grafi a oggetti, qual è la definizione di dimensione di un oggetto? È solo la dimensione dell'istanza dell'oggetto che stai esaminando o la dimensione dell'intero grafico di dati radicato nell'istanza dell'oggetto? Quest'ultimo è ciò che di solito conta di più nella pratica. Come vedrai, le cose non sono sempre così chiare, ma per cominciare puoi seguire questo approccio:

  • Un'istanza di un oggetto può essere dimensionata (approssimativamente) sommando tutti i suoi campi di dati non statici (inclusi i campi definiti nelle superclassi)
  • A differenza, ad esempio, del C ++, i metodi di classe e la loro virtualità non hanno alcun impatto sulla dimensione dell'oggetto
  • Le superinterfacce di classe non hanno alcun impatto sulla dimensione dell'oggetto (vedere la nota alla fine di questo elenco)
  • La dimensione completa dell'oggetto può essere ottenuta come chiusura dell'intero oggetto grafico radicato nell'oggetto iniziale
Nota: l' implementazione di qualsiasi interfaccia Java contrassegna semplicemente la classe in questione e non aggiunge alcun dato alla sua definizione. Infatti, la JVM non convalida nemmeno che un'implementazione dell'interfaccia fornisca tutti i metodi richiesti dall'interfaccia: questa è strettamente responsabilità del compilatore nelle specifiche correnti.

Per avviare il processo, per i tipi di dati primitivi utilizzo le dimensioni fisiche misurate dalla Sizeofclasse di Java Tip 130 . A quanto pare, per le comuni JVM a 32 bit un semplice java.lang.Objectoccupa 8 byte, e i tipi di dati di base sono solitamente della dimensione fisica minima in grado di soddisfare i requisiti della lingua (tranne che booleanoccupa un intero byte):

// java.lang.Object dimensione della shell in byte: public static final int OBJECT_SHELL_SIZE = 8; int finale statico pubblico OBJREF_SIZE = 4; int finale statico pubblico LONG_FIELD_SIZE = 8; int finale statico pubblico INT_FIELD_SIZE = 4; int finale statico pubblico SHORT_FIELD_SIZE = 2; int finale statico pubblico CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; int finale statico pubblico BOOLEAN_FIELD_SIZE = 1; int finale statico pubblico DOUBLE_FIELD_SIZE = 8; int finale statico pubblico FLOAT_FIELD_SIZE = 4;

(È importante rendersi conto che queste costanti non sono codificate per sempre e devono essere misurate indipendentemente per una data JVM.) Ovviamente, la somma ingenua delle dimensioni del campo dell'oggetto trascura i problemi di allineamento della memoria nella JVM. L'allineamento della memoria è importante (come mostrato, ad esempio, per i tipi di array primitivi in ​​Java Tip 130), ma penso che non sia redditizio cercare dettagli di basso livello. Non solo tali dettagli dipendono dal fornitore della JVM, ma non sono sotto il controllo del programmatore. Il nostro obiettivo è ottenere una buona stima delle dimensioni dell'oggetto e, si spera, avere un'idea di quando un campo di classe potrebbe essere ridondante; o quando un campo dovrebbe essere popolato pigramente; o quando è necessaria una struttura dati nidificata più compatta, ecc. Per una precisione fisica assoluta puoi sempre tornare alla Sizeofclasse in Java Tip 130.

Per aiutare a profilare ciò che costituisce un'istanza di oggetto, il nostro strumento non si limiterà a calcolare la dimensione, ma costruirà anche un'utile struttura di dati come sottoprodotto: un grafico composto da IObjectProfileNodes:

interfaccia IObjectProfileNode {Object object (); Nome stringa (); int size (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] children (); Shell IObjectProfileNode (); IObjectProfileNode [] path (); IObjectProfileNode root (); int pathlength (); boolean traverse (filtro INodeFilter, visitatore INodeVisitor); String dump (); } // Fine dell'interfaccia

IObjectProfileNodesono interconnessi quasi esattamente nello stesso modo dell'oggetto grafico originale, con la IObjectProfileNode.object()restituzione dell'oggetto reale rappresentato da ciascun nodo. IObjectProfileNode.size()restituisce la dimensione totale (in byte) della sottostruttura dell'oggetto radicata nell'istanza dell'oggetto di quel nodo. Se un'istanza di un oggetto si collega ad altri oggetti tramite campi istanza non nulli o tramite riferimenti contenuti all'interno di campi array, IObjectProfileNode.children()sarà un elenco corrispondente di nodi del grafico figlio, ordinati in ordine di dimensione decrescente. Viceversa, per ogni nodo diverso da quello di partenza, IObjectProfileNode.parent()restituisce il suo genitore. L'intera raccolta di messaggi IObjectProfileNodequindi suddivide e taglia l'oggetto originale e mostra come la memorizzazione dei dati è partizionata al suo interno. Inoltre, i nomi dei nodi del grafo derivano dai campi della classe ed esaminano il percorso di un nodo all'interno del grafo (IObjectProfileNode.path()) consente di tracciare i collegamenti di proprietà dall'istanza dell'oggetto originale a qualsiasi dato interno.

Avrai notato durante la lettura del paragrafo precedente che l'idea finora ha ancora qualche ambiguità. Se, mentre attraversi l'oggetto grafico, incontri la stessa istanza di oggetto più di una volta (cioè più di un campo da qualche parte nel grafico sta puntando ad essa), come ne assegni la proprietà (il puntatore genitore)? Considera questo snippet di codice:

 Oggetto obj = new String [] {new String ("JavaWorld"), new 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.

Pensare agli attraversamenti del grafo e ai percorsi più brevi dovrebbe suonare un campanello a questo punto: la ricerca breadth-first è un algoritmo di attraversamento del grafico che garantisce di trovare il percorso più breve dal nodo di partenza a qualsiasi altro nodo di grafo raggiungibile.

Dopo tutti questi preliminari, ecco un'implementazione da manuale di tale attraversamento del grafico. (Alcuni dettagli e metodi ausiliari sono stati omessi; vedere il download di questo articolo per i dettagli completi.):