Programmering

Java Tip 130: Kender du din datastørrelse?

For nylig hjalp jeg med at designe en Java-serverapplikation, der lignede en database i hukommelsen. Det vil sige, at vi forudindtager designet mod at cache mange data i hukommelsen for at give superhurtig forespørgsel.

Når vi fik prototypen til at køre, besluttede vi naturligvis at profilere datahukommelsens fodaftryk, efter at den var blevet parset og indlæst fra disken. De utilfredsstillende indledende resultater fik mig imidlertid til at søge efter forklaringer.

Bemærk: Du kan downloade denne artikels kildekode fra Resources.

Værktøjet

Da Java målrettet skjuler mange aspekter af hukommelsesstyring, kræver det noget arbejde at opdage, hvor meget hukommelse dine objekter bruger. Du kan bruge Runtime.freeMemory () metode til at måle bunkestørrelsesforskelle før og efter at flere objekter er tildelt. Flere artikler, såsom Ramchander Varadarajans "Ugens spørgsmål nr. 107" (Sun Microsystems, september 2000) og Tony Sintes "Memory Matters" (JavaWorld, December 2001), detaljer denne idé. Desværre mislykkes den tidligere artikels løsning, fordi implementeringen anvender en forkert Kørselstid metode, mens sidstnævnte artikels løsning har sine egne ufuldkommenheder:

  • Et enkelt opkald til Runtime.freeMemory () viser sig utilstrækkelig, fordi en JVM kan beslutte at øge sin nuværende bunkestørrelse til enhver tid (især når den kører affaldsindsamling). Medmindre den samlede dyngestørrelse allerede er på maksimum -Xmx, skal vi bruge Runtime.totalMemory () - Runtime.freeMemory () som den brugte bunke størrelse.
  • Udfører en enkelt Runtime.gc () opkald viser sig muligvis ikke tilstrækkeligt aggressivt til at anmode om affaldsindsamling. Vi kunne f.eks. Anmode om, at objektfinaliserere også køres. Og siden Runtime.gc () ikke er dokumenteret at blokere, indtil indsamlingen er afsluttet, er det en god ide at vente, indtil den opfattede bunkestørrelse stabiliseres.
  • Hvis den profilerede klasse opretter statiske data som en del af sin initialisering pr. Klasse (inklusive statiske klasse- og feltinitialiserere), kan den heaphukommelse, der bruges til den første klasses forekomst, omfatte disse data. Vi bør ignorere bunkeplads, der forbruges af første klasses forekomst.

I betragtning af disse problemer præsenterer jeg Størrelse af, et værktøj, hvormed jeg snoker på forskellige Java-kerne- og applikationsklasser:

public class Sizeof {public static void main (String [] args) throw Exception {// Opvarm alle klasser / metoder, vi vil bruge runGC (); usedMemory (); // Array for at holde stærke referencer til tildelte objekter endelig intantal = 100000; Objekt [] objekter = nyt objekt [antal]; lang bunke1 = 0; // Tildel antal + 1 objekter, kassér den første for (int i = -1; i = 0) objekter [i] = objekt; ellers {object = null; // Kass opvarmningsobjektet runGC (); heap1 = brugtMemory (); // Tag et snapshot før heap}} runGC (); lang heap2 = brugtMemory (); // Tag et snapshot efter bunken: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'før' bunke:" + bunke1 + ", 'efter' bunke:" + bunke2); System.out.println ("heap delta:" + (heap2 - heap1) + ", {" + objects [0] .getClass () + "} size =" + size + "bytes"); for (int i = 0; i <count; ++ i) objekter [i] = null; objekter = null; } privat statisk ugyldig runGC () kaster undtagelse {// Det hjælper med at ringe til Runtime.gc () // ved hjælp af flere metodeopkald: for (int r = 0; r <4; ++ r) _runGC (); } privat statisk ugyldigt _runGC () kaster Undtagelse {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 (); }} privat statisk længe brugtMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privat statisk endelig Runtime s_runtime = Runtime.getRuntime (); } // Afslutning af undervisningen 

Størrelse af's vigtigste metoder er runGC () og usedMemory (). Jeg bruger en runGC () indpakningsmetode at ringe til _runGC () flere gange, fordi det ser ud til at gøre metoden mere aggressiv. (Jeg er ikke sikker på hvorfor, men det er muligt at oprette og ødelægge en metode call-stack-ramme forårsager en ændring i tilgængelighedsrodsættet og beder affaldssamleren om at arbejde hårdere. Desuden forbruger en stor del af bunken plads til at skabe nok arbejde for affaldssamleren at sparke ind hjælper også. Generelt er det svært at sikre, at alt bliver samlet. De nøjagtige detaljer afhænger af JVM og algoritme til indsamling af affald.)

Bemærk nøje de steder, hvor jeg påberåber mig runGC (). Du kan redigere koden mellem bunke1 og bunke2 erklæringer for at instantiere noget af interesse.

Bemærk også hvordan Størrelse af udskriver objektstørrelsen: den transitive lukning af data, der kræves af alle tælle klasseinstanser divideret med tælle. For de fleste klasser forbruges hukommelsen af ​​en enkelt klasseinstans, inklusive alle dens ejede felter. Denne værdi for hukommelsesaftryk adskiller sig fra data fra mange kommercielle profiler, der rapporterer lave hukommelsesfodspor (f.eks. Hvis et objekt har en int [] felt vises hukommelsesforbruget separat).

Resultaterne

Lad os anvende dette enkle værktøj på et par klasser, og se om resultaterne svarer til vores forventninger.

Bemærk: Følgende resultater er baseret på Suns JDK 1.3.1 til Windows. På grund af hvad der er og ikke garanteres af Java-sproget og JVM-specifikationerne, kan du ikke anvende disse specifikke resultater på andre platforme eller andre Java-implementeringer.

java.lang.Objekt

Roden til alle objekter skulle bare være min første sag. Til java.lang.Objekt, Jeg får:

'før' bunke: 510696, 'efter' bunke: 1310696 bunke delta: 800000, {klasse java.lang.Object} størrelse = 8 byte 

Så en almindelig Objekt tager 8 byte; selvfølgelig bør ingen forvente, at størrelsen er 0, da hver instans skal bære rundt på felter, der understøtter basisoperationer som f.eks lige med(), hashCode (), vent () / underret (), og så videre.

java.lang. heltal

Mine kolleger og jeg pakker ofte indfødte ints ind i Heltal forekomster, så vi kan gemme dem i Java-samlinger. Hvor meget koster det os i hukommelsen?

'før' bunke: 510696, 'efter' bunke: 2110696 bunke delta: 1600000, {klasse java.lang.Integer} størrelse = 16 byte 

16-byte-resultatet er lidt dårligere end forventet, fordi et int værdi kan passe i kun 4 ekstra byte. Brug af en Heltal koster mig en hukommelsesomkostning på 300 procent sammenlignet med når jeg kan gemme værdien som en primitiv type.

java.lang. lang

Lang skulle tage mere hukommelse end Heltal, men det gør det ikke:

'før' bunke: 510696, 'efter' bunke: 2110696 bunke delta: 1600000, {klasse java.lang.Long} størrelse = 16 byte 

Det er klart, at den faktiske objektstørrelse på bunken er underlagt hukommelsesjustering på lavt niveau udført af en bestemt JVM-implementering for en bestemt CPU-type. Det ligner en Lang er 8 byte på Objekt overhead plus 8 byte mere til den faktiske lange værdi. I modsætning, Heltal havde et ubrugt 4-byte hul, sandsynligvis fordi JVM I bruger kræfter, der er justeret på en 8-byte ordgrænse.

Arrays

At spille med primitive type arrays viser sig at være lærerigt, dels for at opdage skjulte overhead og dels for at retfærdiggøre et andet populært trick: indpakning af primitive værdier i en størrelse 1-array for at bruge dem som objekter. Ved at ændre Størrelse af hoved () at have en løkke, der forøger den oprettede matrixlængde på hver iteration, får jeg for int arrays:

længde: 0, {klasse [I} størrelse = 16 byte længde: 1, {klasse [I} størrelse = 16 byte længde: 2, {klasse [I} størrelse = 24 byte længde: 3, {klasse [I} størrelse = 24 bytes længde: 4, {klasse [I} størrelse = 32 bytes længde: 5, {klasse [I} størrelse = 32 bytes længde: 6, {klasse [I} størrelse = 40 bytes længde: 7, {klasse [I} størrelse = 40 bytes længde: 8, {klasse [I} størrelse = 48 bytes længde: 9, {klasse [I} størrelse = 48 bytes længde: 10, {klasse [I} størrelse = 56 bytes 

og for char arrays:

længde: 0, {klasse [C} størrelse = 16 byte længde: 1, {klasse [C} størrelse = 16 byte længde: 2, {klasse [C} størrelse = 16 byte længde: 3, {klasse [C} størrelse = 24 bytes længde: 4, {klasse [C} størrelse = 24 bytes længde: 5, {klasse [C} størrelse = 24 bytes længde: 6, {klasse [C} størrelse = 24 bytes længde: 7, {klasse [C} størrelse = 32 bytes længde: 8, {klasse [C} størrelse = 32 bytes længde: 9, {klasse [C} størrelse = 32 bytes længde: 10, {klasse [C} størrelse = 32 bytes 

Ovenfor dukker beviset for 8-byte-tilpasning op igen. Ud over det uundgåelige Objekt 8-byte overhead, tilføjer et primitivt array yderligere 8 byte (hvoraf mindst 4 byte understøtter længde Mark). Og ved hjælp af int [1] synes ikke at give nogen hukommelsesfordele i forhold til en Heltal eksempel, undtagen måske som en ændret version af de samme data.

Flerdimensionelle arrays

Flerdimensionelle arrays tilbyder en anden overraskelse. Udviklere anvender ofte konstruktioner som int [dim1] [dim2] inden for numerisk og videnskabelig databehandling. I en int [dim1] [dim2] array-forekomst, hver indlejret int [dim2] array er en Objekt i sig selv. Hver tilføjer det sædvanlige 16-byte array overhead. Når jeg ikke har brug for et trekantet eller ujævn array, repræsenterer det rent overhead. Virkningen vokser, når arraydimensionerne er meget forskellige. F.eks int [128] [2] instans tager 3.600 bytes. Sammenlignet med de 1.040 bytes en int [256] instansanvendelser (som har samme kapacitet), 3.600 byte repræsenterer en overhead på 246 procent. I ekstreme tilfælde af byte [256] [1], overheadfaktoren er næsten 19! Sammenlign det med C / C ++ - situationen, hvor den samme syntaks ikke tilføjer nogen lageromkostninger.

java.lang.Streng

Lad os prøve en tom Snor, først konstrueret som ny streng ():

'før' bunke: 510696, 'efter' bunke: 4510696 bunke delta: 4000000, {klasse java.lang.String} størrelse = 40 byte 

Resultatet viser sig ret deprimerende. En tom Snor tager 40 byte - nok hukommelse til at passe til 20 Java-tegn.

Før jeg prøver Snors med indhold har jeg brug for en hjælpemetode til at oprette Snors garanteres ikke at blive interneret. Brug kun bogstaver som i:

 object = "streng med 20 tegn"; 

fungerer ikke, fordi alle sådanne objekthåndtag ender med at pege på det samme Snor eksempel. Sprogspecifikationen dikterer en sådan adfærd (se også java.lang.String.intern () metode). Derfor, hvis du vil fortsætte vores hukommelse, snooping, skal du prøve

 offentlig statisk streng createString (endelig int længde) {char [] resultat = ny char [længde]; for (int i = 0; i <længde; ++ i) resultat [i] = (char) i; returner ny streng (resultat); } 

Efter at have bevæbnet mig med dette Snor skabermetode, jeg får følgende resultater:

længde: 0, {klasse java.lang.String} størrelse = 40 bytes længde: 1, {klasse java.lang.String} størrelse = 40 bytes længde: 2, {klasse java.lang.String} størrelse = 40 bytes længde: 3, {klasse java.lang.String} størrelse = 48 bytes længde: 4, {klasse java.lang.String} størrelse = 48 bytes længde: 5, {klasse java.lang.String} størrelse = 48 bytes længde: 6, {klasse java.lang.String} størrelse = 48 bytes længde: 7, {klasse java.lang.String} størrelse = 56 bytes længde: 8, {klasse java.lang.String} størrelse = 56 bytes længde: 9, {klasse java.lang.String} størrelse = 56 bytes længde: 10, {klasse java.lang.String} størrelse = 56 bytes 

Resultaterne viser tydeligt, at a Snorhukommelsesvækst sporer dens interne char array's vækst. Men den Snor klasse tilføjer yderligere 24 byte overhead. For en ikke-uberettiget Snor med størrelse 10 tegn eller mindre, de ekstra omkostninger i forhold til nyttelasten (2 byte for hver char plus 4 byte for længden), varierer fra 100 til 400 procent.

Selvfølgelig afhænger sanktionen af ​​din applikations datadistribution. På en eller anden måde formodede jeg, at 10 tegn repræsenterer det typiske Snor længde til en række applikationer. For at få et konkret datapunkt instrumenterede jeg SwingSet2 demo (ved at ændre Snor klasseimplementering direkte), der fulgte med JDK 1.3.x for at spore længderne på Snors det skaber. Efter et par minutter at lege med demoen viste en datadump, at omkring 180.000 Strenge blev instantieret. At sortere dem i størrelsesspande bekræftede mine forventninger:

[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 ... 

Det er rigtigt, mere end 50 procent af alle Snor længder faldt i 0-10 spanden, det meget hot spot af Snor klasse ineffektivitet!

I virkeligheden, Snors kan forbruge endnu mere hukommelse end deres længder antyder: Snors genereret ud af StringBuffers (enten eksplicit eller via '+' sammenkædningsoperatøren) sandsynligvis har char arrays med længder større end den rapporterede Snor længder fordi StringBuffers starter typisk med en kapacitet på 16, og fordobles derefter Tilføj() operationer. Så for eksempel createString (1) + '' ender med en char matrix af størrelse 16, ikke 2.

Hvad gør vi?

"Dette er alt sammen meget godt, men vi har ikke andet valg end at bruge Snors og andre typer leveret af Java, gør vi? "Jeg hører dig spørge. Lad os finde ud af det.

Indpakningsklasser