Suggerimento Java 130: conosci la dimensione dei tuoi dati?

Di recente, ho contribuito a progettare un'applicazione server Java che somigliava a un database in memoria. Cioè, abbiamo orientato il progetto verso la memorizzazione nella cache di tonnellate di dati in memoria per fornire prestazioni di query super veloci.

Una volta avviato il prototipo, abbiamo naturalmente deciso di profilare l'impronta della memoria dati dopo che era stata analizzata e caricata dal disco. I risultati iniziali insoddisfacenti, tuttavia, mi hanno spinto a cercare spiegazioni.

Nota: puoi scaricare il codice sorgente di questo articolo da Risorse.

Lo strumento

Poiché Java nasconde intenzionalmente molti aspetti della gestione della memoria, scoprire quanta memoria consumano i tuoi oggetti richiede del lavoro. È possibile utilizzare il Runtime.freeMemory()metodo per misurare le differenze di dimensioni dell'heap prima e dopo che diversi oggetti sono stati allocati. Diversi articoli, come "Question of the Week No. 107" di Ramchander Varadarajan (Sun Microsystems, settembre 2000) e "Memory Matters" di Tony Sintes ( JavaWorld, dicembre 2001), dettagliano questa idea. Sfortunatamente, la soluzione del primo articolo fallisce perché l'implementazione utilizza un Runtimemetodo sbagliato , mentre la soluzione del secondo articolo ha le sue imperfezioni:

  • Una singola chiamata a si Runtime.freeMemory()rivela insufficiente perché una JVM può decidere di aumentare la sua dimensione heap corrente in qualsiasi momento (specialmente quando esegue la garbage collection). A meno che la dimensione dell'heap totale non sia già alla dimensione massima di -Xmx, dovremmo usarla Runtime.totalMemory()-Runtime.freeMemory()come dimensione dell'heap usata.
  • L'esecuzione di una singola Runtime.gc()chiamata potrebbe non risultare sufficientemente aggressiva per la richiesta di Garbage Collection. Potremmo, ad esempio, richiedere l'esecuzione anche dei finalizzatori di oggetti. E poiché Runtime.gc()non è documentato che si blocchi fino al completamento della raccolta, è una buona idea attendere fino a quando la dimensione dell'heap percepita si stabilizza.
  • Se la classe profilata crea dati statici come parte dell'inizializzazione della classe per classe (inclusi gli inizializzatori di campo e di classe statici), la memoria di heap utilizzata per l'istanza di prima classe può includere tali dati. Dobbiamo ignorare lo spazio heap consumato dall'istanza di prima classe.

Considerando questi problemi, vi presento Sizeofuno strumento con cui curiosare in vari core Java e classi di applicazioni:

public class Sizeof {public static void main (String [] args) genera un'eccezione {// Riscalda tutte le classi / metodi che useremo runGC (); usedMemory (); // Array per mantenere forti riferimenti agli oggetti allocati final int count = 100000; Object [] objects = new Object [count]; heap1 lungo = 0; // Alloca count + 1 oggetti, scarta il primo per (int i = -1; i = 0) objects [i] = object; altro {oggetto = null; // Elimina l'oggetto di riscaldamento runGC (); heap1 = usedMemory (); // Acquisisce un'istantanea prima dell'heap}} runGC (); long heap2 = usedMemory (); // Scatta un'istantanea dopo l'heap: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'prima' heap:" + heap1 + ", 'dopo' heap:" + heap2); System.out.println ("delta heap:" + (heap2 - heap1) + ", {" + oggetti [0].getClass () + "} size =" + size + "byte"); for (int i = 0; i <count; ++ i) oggetti [i] = null; oggetti = null; } private static void runGC () genera un'eccezione {// Aiuta a chiamare Runtime.gc () // utilizzando diverse chiamate di metodo: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () genera Eccezione {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezioneio <count; ++ i) oggetti [i] = null; oggetti = null; } private static void runGC () genera un'eccezione {// Aiuta a chiamare Runtime.gc () // utilizzando diverse chiamate di metodo: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () genera un'eccezione {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezioneio <count; ++ i) oggetti [i] = null; oggetti = null; } private static void runGC () genera un'eccezione {// Aiuta a chiamare Runtime.gc () // utilizzando diverse chiamate di metodo: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () genera Eccezione {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezionegc () // utilizzando diverse chiamate di metodo: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () genera Eccezione {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezionegc () // utilizzando diverse chiamate di metodo: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () genera un'eccezione {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezioneThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezioneThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } Runtime finale statico privato s_runtime = Runtime.getRuntime (); } // Fine della lezione

SizeofI metodi chiave di sono runGC()e usedMemory(). Uso un runGC()metodo wrapper per chiamare _runGC()più volte perché sembra rendere il metodo più aggressivo. (Non sono sicuro del perché, ma è possibile creare e distruggere un frame dello stack di chiamate del metodo causa un cambiamento nel set di root di raggiungibilità e richiede al garbage collector di lavorare di più. Inoltre, consumando una grande frazione dello spazio di heap per creare abbastanza lavoro anche per il garbage collector può essere utile. In generale, è difficile garantire che tutto venga raccolto. I dettagli esatti dipendono dalla JVM e dall'algoritmo di garbage collection.)

Nota attentamente i luoghi in cui invoco runGC(). È possibile modificare il codice tra le dichiarazioni heap1e heap2per istanziare qualsiasi cosa di interesse.

Nota anche come Sizeofstampa la dimensione dell'oggetto: la chiusura transitiva dei dati richiesta da tutte countle istanze di classe, divisa per count. Per la maggior parte delle classi, il risultato sarà la memoria consumata da una singola istanza di classe, inclusi tutti i campi di proprietà. Il valore dell'impronta di memoria differisce dai dati forniti da molti profiler commerciali che segnalano impronte di memoria superficiali (ad esempio, se un oggetto ha un int[]campo, il suo consumo di memoria apparirà separatamente).

I risultati

Applichiamo questo semplice strumento ad alcune classi, quindi vediamo se i risultati corrispondono alle nostre aspettative.

Nota: i seguenti risultati si basano su JDK 1.3.1 di Sun per Windows. A causa di ciò che è e non è garantito dal linguaggio Java e dalle specifiche JVM, non è possibile applicare questi risultati specifici ad altre piattaforme o altre implementazioni Java.

java.lang.Object

Ebbene, la radice di tutti gli oggetti doveva essere il mio primo caso. Per java.lang.Object, ottengo:

heap "prima": 510696, heap "dopo": 1310696 delta heap: 800000, dimensione {class java.lang.Object} = 8 byte 

Quindi, un semplice Objectrichiede 8 byte; Naturalmente, nessuno dovrebbe aspettarsi la dimensione per essere 0, come ogni istanza deve portare in giro i campi che le operazioni di base di sostegno, come equals(), hashCode(), wait()/notify()e così via.

java.lang.Integer

I miei colleghi e io spesso inseriamo elementi nativi intsin Integeristanze in modo da poterli archiviare in raccolte Java. Quanto ci costa in memoria?

heap "prima": 510696, heap "dopo": 2110696 delta heap: 1600000, dimensione {class java.lang.Integer} = 16 byte 

Il risultato a 16 byte è leggermente peggiore di quanto mi aspettassi perché un intvalore può rientrare in soli 4 byte extra. L'utilizzo di un Integermi costa un overhead di memoria del 300% rispetto a quando posso memorizzare il valore come un tipo primitivo.

java.lang.Long

Longdovrebbe richiedere più memoria di Integer, ma non lo fa:

heap "prima": 510696, heap "dopo": 2110696 delta dell'heap: 1600000, {class java.lang.Long} dimensione = 16 byte 

Chiaramente, la dimensione effettiva dell'oggetto sull'heap è soggetta all'allineamento della memoria di basso livello eseguito da una particolare implementazione JVM per un particolare tipo di CPU. Sembra che a Longsia 8 byte di Objectoverhead, più 8 byte in più per il valore long effettivo. Al contrario, Integeraveva un buco di 4 byte inutilizzato, molto probabilmente perché la JVM che uso forza l'allineamento degli oggetti su un confine di parola di 8 byte.

Array

Giocare con array di tipi primitivi si rivela istruttivo, in parte per scoprire qualsiasi overhead nascosto e in parte per giustificare un altro trucco popolare: racchiudere valori primitivi in ​​un array di dimensione 1 per usarli come oggetti. Modificando Sizeof.main()per avere un ciclo che incrementa la lunghezza dell'array creato ad ogni iterazione, ottengo per gli intarray:

lunghezza: 0, {classe [I} dimensione = 16 byte lunghezza: 1, {classe [I} dimensione = 16 byte lunghezza: 2, {classe [I} dimensione = 24 byte lunghezza: 3, {classe [I} dimensione = Lunghezza 24 byte: 4, {dimensione classe [I} = lunghezza 32 byte: 5, {dimensione classe [I} = lunghezza 32 byte: 6, {dimensione classe [I} = lunghezza 40 byte: 7, {classe [I} dimensione = 40 byte lunghezza: 8, {classe [I} dimensione = 48 byte lunghezza: 9, {classe [I} dimensione = 48 byte lunghezza: 10, {classe [I} dimensione = 56 byte 

e per gli chararray:

lunghezza: 0, {classe [C} dimensione = 16 byte lunghezza: 1, {classe [C} dimensione = 16 byte lunghezza: 2, {classe [C} dimensione = 16 byte lunghezza: 3, {classe [C} dimensione = Lunghezza 24 byte: 4, {dimensione classe [C} = lunghezza 24 byte: 5, {dimensione classe [C} = lunghezza 24 byte: 6, {dimensione classe [C} = lunghezza 24 byte: 7, {classe [C} dimensione = 32 byte lunghezza: 8, {classe [C} dimensione = 32 byte lunghezza: 9, {classe [C} dimensione = 32 byte lunghezza: 10, {classe [C} dimensione = 32 byte 

Sopra, viene nuovamente visualizzata la prova dell'allineamento a 8 byte. Inoltre, oltre all'inevitabile Objectoverhead di 8 byte, un array primitivo aggiunge altri 8 byte (di cui almeno 4 byte supportano il lengthcampo). E l'utilizzo int[1]sembra non offrire alcun vantaggio di memoria su Integerun'istanza, tranne forse come versione modificabile degli stessi dati.

Array multidimensionali

Multidimensional arrays offer another surprise. Developers commonly employ constructs like int[dim1][dim2] in numerical and scientific computing. In an int[dim1][dim2] array instance, every nested int[dim2] array is an Object in its own right. Each adds the usual 16-byte array overhead. When I don't need a triangular or ragged array, that represents pure overhead. The impact grows when array dimensions greatly differ. For example, a int[128][2] instance takes 3,600 bytes. Compared to the 1,040 bytes an int[256] instance uses (which has the same capacity), 3,600 bytes represent a 246 percent overhead. In the extreme case of byte[256][1], the overhead factor is almost 19! Compare that to the C/C++ situation in which the same syntax does not add any storage overhead.

java.lang.String

Let's try an empty String, first constructed as new String():

'before' heap: 510696, 'after' heap: 4510696 heap delta: 4000000, {class java.lang.String} size = 40 bytes 

The result proves quite depressing. An empty String takes 40 bytes—enough memory to fit 20 Java characters.

Before I try Strings with content, I need a helper method to create Strings guaranteed not to get interned. Merely using literals as in:

 object = "string with 20 chars"; 

will not work because all such object handles will end up pointing to the same String instance. The language specification dictates such behavior (see also the java.lang.String.intern() method). Therefore, to continue our memory snooping, try:

 public static String createString (final int length) { char [] result = new char [length]; for (int i = 0; i < length; ++ i) result [i] = (char) i; return new String (result); } 

After arming myself with this String creator method, I get the following results:

length: 0, {class java.lang.String} size = 40 bytes length: 1, {class java.lang.String} size = 40 bytes length: 2, {class java.lang.String} size = 40 bytes length: 3, {class java.lang.String} size = 48 bytes length: 4, {class java.lang.String} size = 48 bytes length: 5, {class java.lang.String} size = 48 bytes length: 6, {class java.lang.String} size = 48 bytes length: 7, {class java.lang.String} size = 56 bytes length: 8, {class java.lang.String} size = 56 bytes length: 9, {class java.lang.String} size = 56 bytes length: 10, {class java.lang.String} size = 56 bytes 

The results clearly show that a String's memory growth tracks its internal char array's growth. However, the String class adds another 24 bytes of overhead. For a nonempty String of size 10 characters or less, the added overhead cost relative to useful payload (2 bytes for each char plus 4 bytes for the length), ranges from 100 to 400 percent.

Of course, the penalty depends on your application's data distribution. Somehow I suspected that 10 characters represents the typical String length for a variety of applications. To get a concrete data point, I instrumented the SwingSet2 demo (by modifying the String class implementation directly) that came with JDK 1.3.x to track the lengths of the Strings it creates. After a few minutes playing with the demo, a data dump showed that about 180,000 Strings were instantiated. Sorting them into size buckets confirmed my expectations:

[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ... 

That's right, more than 50 percent of all String lengths fell into the 0-10 bucket, the very hot spot of String class inefficiency!

In realtà, Stringi messaggi di posta elettronica possono consumare anche più memoria di quanto suggeriscono le loro lunghezze: Stringi messaggi generati da StringBuffers (esplicitamente o tramite l'operatore di concatenazione '+') probabilmente hanno chararray con lunghezze maggiori di quelle riportate Stringperché in StringBuffergenere iniziano con una capacità di 16 , quindi raddoppialo sulle append()operazioni. Quindi, ad esempio, createString(1) + ' 'finisce con un chararray di dimensione 16, non 2.

Cosa facciamo?

"Va tutto molto bene, ma non abbiamo altra scelta che usare se Stringaltri tipi forniti da Java, vero?" Ti sento chiedere. Scopriamolo.

Classi wrapper