Programmering

Java Tips 130: Kjenner du datastørrelsen din?

Nylig hjalp jeg til med å designe et Java-serverprogram som lignet en database i minnet. Det vil si at vi forutsette designet mot å lagre tonnevis av data i minnet for å gi superrask spørringsytelse.

Når vi fikk prototypen i gang, bestemte vi oss naturlig for å profilere dataminnefotavtrykket etter at det var blitt analysert og lastet fra disk. De utilfredsstillende innledende resultatene fikk meg imidlertid til å søke etter forklaringer.

Merk: Du kan laste ned kildekoden til denne artikkelen fra Resources.

Verktøyet

Siden Java målrettet skjuler mange aspekter av minneadministrasjon, tar det litt arbeid å oppdage hvor mye minne objektene dine bruker. Du kan bruke Runtime.freeMemory () metode for å måle haugstørrelsesforskjeller før og etter at flere objekter er tildelt. Flere artikler, som Ramchander Varadarajans "Ukens spørsmål nr. 107" (Sun Microsystems, september 2000) og Tony Sintes "Memory Matters" (JavaWorld, Desember 2001), detaljert ideen. Dessverre mislykkes den tidligere artikkelens løsning fordi implementeringen benytter en feil Kjøretid metode, mens sistnevnte artikkels løsning har sine egne ufullkommenheter:

  • En enkelt samtale til Runtime.freeMemory () viser seg å være utilstrekkelig fordi en JVM kan bestemme seg for å øke sin nåværende haugestørrelse når som helst (spesielt når den kjører søppelinnsamling). Med mindre den totale dyngestørrelsen allerede er på maksimum -Xmx, bør vi bruke den Runtime.totalMemory () - Runtime.freeMemory () som brukt haugestørrelse.
  • Å utføre en singel Runtime.gc () samtale kan ikke vise seg å være tilstrekkelig aggressiv for å be om søppel. Vi kan for eksempel be om at objektfinaliserere også skal kjøre. Og siden Runtime.gc () ikke er dokumentert for å blokkere før samlingen er fullført, er det lurt å vente til den oppfattede haugstørrelsen stabiliserer seg.
  • Hvis den profilerte klassen oppretter statiske data som en del av initialiseringen av klasse per klasse (inkludert statisk klasse- og feltinitialisering), kan heapminnet som brukes i førsteklasses forekomst, inkludere dataene. Vi bør se bort fra massevis av forbruk av førsteklasses forekomst.

Med tanke på disse problemene presenterer jeg Størrelsen av, et verktøy som jeg lurer på i forskjellige Java-kjerne- og applikasjonsklasser:

offentlig klasse Sizeof {offentlig statisk tomrom hoved (String [] args) kaster Unntak {// Varm opp alle klasser / metoder vi vil bruke runGC (); usedMemory (); // Array for å holde sterke referanser til tildelte objekter endelig intelling = 100000; Objekt [] objekter = nytt objekt [antall]; lang haug1 = 0; // Tildel antall + 1 objekter, kast den første for (int i = -1; i = 0) objekter [i] = objekt; annet {objekt = null; // Kast oppvarmingsobjektet runGC (); heap1 = usedMemory (); // Ta et øyeblikksbilde før heap}} runGC (); lang heap2 = usedMemory (); // Ta et øyeblikksbilde etter heap: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'før' heap:" + heap1 + ", 'etter' heap:" + heap2); 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 Unntak {// Det hjelper å ringe Runtime.gc () // ved å bruke flere metodeanrop: for (int r = 0; r <4; ++ r) _runGC (); } privat statisk tomrom _runGC () kaster Unntak {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 lenge bruktMinne () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privat statisk slutt Runtime s_runtime = Runtime.getRuntime (); } // Slutten på timen 

Størrelsen avs viktigste metoder er runGC () og usedMemory (). Jeg bruker en runGC () innpakningsmetode å ringe _runGC () flere ganger fordi det ser ut til å gjøre metoden mer aggressiv. (Jeg er ikke sikker på hvorfor, men det er mulig å opprette og ødelegge en metode call-stack-ramme forårsaker en endring i rotsettet som kan nås og ber søppeloppsamleren om å jobbe hardere. Dessuten bruker en stor del av haugplassen for å skape nok arbeid for søppeloppsamleren å sparke inn hjelper også. Generelt er det vanskelig å sikre at alt blir samlet inn. De nøyaktige detaljene avhenger av JVM og algoritme for innsamling av søppel.)

Legg nøye merke til stedene jeg påberoper meg runGC (). Du kan redigere koden mellom haug1 og heap2 erklæringer for å sette i gang noe av interesse.

Legg også merke til hvordan Størrelsen av skriver ut objektstørrelsen: den transitive lukkingen av data som alle krever telle klasseinstanser, delt på telle. For de fleste klasser vil resultatet bli brukt av en enkelt klasseinstans, inkludert alle de eide feltene. Verdien for minnefotavtrykk skiller seg fra data levert av mange kommersielle profilere som rapporterer grunne minnefotavtrykk (for eksempel hvis et objekt har en int [] felt vises minneforbruket separat).

Resultatene

La oss bruke dette enkle verktøyet på noen få klasser, og se om resultatene samsvarer med forventningene våre.

Merk: Følgende resultater er basert på Suns JDK 1.3.1 for Windows. På grunn av hva som er og ikke garanteres av Java-språket og JVM-spesifikasjonene, kan du ikke bruke disse spesifikke resultatene på andre plattformer eller andre Java-implementeringer.

java.lang.Objekt

Vel, roten til alle objekter måtte bare være mitt første tilfelle. Til java.lang.Objekt, Jeg får:

'før' heap: 510696, 'after' heap: 1310696 heap delta: 800000, {class java.lang.Object} size = 8 bytes 

Altså, en slette Gjenstand tar 8 byte; selvfølgelig skal ingen forvente at størrelsen er 0, da hver forekomst må bære rundt felt som støtter basisoperasjoner som er lik(), hashCode (), vent () / varsle (), og så videre.

java.lang. heltall

Mine kolleger og jeg pakker ofte innfødte ints inn i Heltall tilfeller slik at vi kan lagre dem i Java-samlinger. Hvor mye koster det oss i minnet?

'før' heap: 510696, 'after' heap: 2110696 heap delta: 1600000, {class java.lang.Integer} size = 16 bytes 

16-byte-resultatet er litt dårligere enn jeg forventet fordi et int verdien kan passe inn i bare 4 ekstra byte. Bruke en Heltall koster meg 300 prosent minneoverhead sammenlignet med når jeg kan lagre verdien som en primitiv type.

java.lang. lang

Lang burde ta mer minne enn Heltall, men det gjør det ikke:

'før' heap: 510696, 'after' heap: 2110696 heap delta: 1600000, {class java.lang.Long} size = 16 bytes 

Det er klart at den faktiske objektstørrelsen på dyngen er underlagt minneinnstilling på lavt nivå, utført av en bestemt JVM-implementering for en bestemt CPU-type. Det ser ut som en Lang er 8 byte av Gjenstand overhead, pluss 8 byte mer for den faktiske lange verdien. I motsetning, Heltall hadde et ubrukt 4-byte hull, mest sannsynlig fordi JVM jeg bruker krefter objektjustering på en 8-byte ordgrense.

Arrays

Å spille med primitive type matriser viser seg å være lærerikt, dels for å oppdage skjulte overhead, dels for å rettferdiggjøre et annet populært triks: innpakning av primitive verdier i en størrelse 1-array for å bruke dem som objekter. Ved å endre Sizeof.main () å ha en sløyfe som øker den opprettede arraylengden på hver iterasjon, får jeg for int matriser:

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

og for røye matriser:

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

Ovenfor dukker bevisene for 8-byte-justering opp igjen. I tillegg til det uunngåelige Gjenstand 8-byte overhead, legger et primitivt utvalg til ytterligere 8 byte (hvorav minst 4 byte støtter lengde felt). Og bruker int [1] ser ut til å ikke gi noen minnefordeler fremfor en Heltall eksempel, bortsett fra kanskje som en muterbar versjon av de samme dataene.

Flerdimensjonale matriser

Flerdimensjonale matriser gir en annen overraskelse. Utviklere bruker ofte konstruksjoner som int [dim1] [dim2] innen numerisk og vitenskapelig databehandling. I en int [dim1] [dim2] array-forekomst, hver nestet int [dim2] array er en Gjenstand i seg selv. Hver legger til den vanlige 16-byte matrisen overhead. Når jeg ikke trenger et trekantet eller ujevnt utvalg, representerer det rent overhead. Virkningen vokser når matrisedimensjoner er veldig forskjellige. For eksempel, a int [128] [2] forekomst tar 3600 byte. Sammenlignet med 1 040 byte int [256] instansbruk (som har samme kapasitet), representerer 3600 byte 246 prosent overhead. I ekstreme tilfeller av byte [256] [1], overheadfaktoren er nesten 19! Sammenlign det med C / C ++ situasjonen der den samme syntaksen ikke legger til lagringsomkostninger.

java.lang.Streng

La oss prøve en tom String, først konstruert som ny streng ():

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

Resultatet viser seg ganske deprimerende. En tom String tar 40 byte - nok minne til å passe 20 Java-tegn.

Før jeg prøver Strings med innhold, trenger jeg en hjelpemetode for å lage Stringer garantert ikke internert. Bare å bruke bokstaver som i:

 objekt = "streng med 20 tegn"; 

vil ikke fungere fordi alle slike objekthåndtak vil ende opp med å peke på det samme String forekomst. Språkspesifikasjonen dikterer slik atferd (se også java.lang.String.intern () metode). Derfor, for å fortsette minnesnokingen, kan du prøve:

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

Etter å ha bevæpnet meg med dette String skapermetode, får jeg følgende resultater:

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

Resultatene viser tydelig at a Stringhukommelsesvekst sporer dens interne røye matrisens vekst. Imidlertid, den String klasse legger til ytterligere 24 byte med overhead. For en nonempty String på størrelse med 10 tegn eller mindre, den ekstra kostnaden i forhold til nyttig nyttelast (2 byte for hver røye pluss 4 byte for lengden), varierer fra 100 til 400 prosent.

Selvfølgelig avhenger straffen av applikasjonens datadistribusjon. På en eller annen måte mistenkte jeg at 10 tegn representerer det typiske String lengde for en rekke bruksområder. For å få et konkret datapunkt, instrumenterte jeg SwingSet2-demoen (ved å endre String klasseimplementering direkte) som fulgte med JDK 1.3.x for å spore lengden på Strings det skaper. Etter noen minutter med å leke med demoen, viste en datadump at om lag 180 000 Strenger ble instantiert. Å sortere dem i størrelsesbøtter bekreftet forventningene mine:

[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 stemmer, mer enn 50 prosent av alle String lengder falt i 0-10 bøtta, det veldig hete stedet for String klasse ineffektivitet!

I virkeligheten, Strings kan forbruke enda mer minne enn lengdene antyder: Strings generert ut av StringBuffers (eksplisitt eller via '+' sammenkoblingsoperatøren) sannsynligvis har røye matriser med lengder større enn rapportert String lengder fordi StringBuffers starter vanligvis med en kapasitet på 16, og deretter dobler du den på legg til () operasjoner. Så for eksempel createString (1) + '' ender opp med en røye matrise av størrelse 16, ikke 2.

Hva skal vi gjøre?

"Dette er veldig bra, men vi har ikke noe annet valg enn å bruke Strings og andre typer levert av Java, gjør vi? "Jeg hører deg spør. La oss finne ut.

Pakkeklasser

$config[zx-auto] not found$config[zx-overlay] not found