Programovanie

Java Tip 130: Poznáte svoju veľkosť dát?

Nedávno som pomohol navrhnúť serverovú aplikáciu Java, ktorá sa podobala databáze v pamäti. To znamená, že sme zaujali dizajn smerom k ukladaniu ton dát do pamäte, aby sme poskytli super rýchly výkon dotazov.

Po spustení prototypu sme sa prirodzene rozhodli profilovať stopu dátovej pamäte po jej analýze a načítaní z disku. Neuspokojivé počiatočné výsledky ma však podnietili k hľadaniu vysvetlení.

Poznámka: Zdrojový kód tohto článku si môžete stiahnuť zo zdrojov.

Nástroj

Pretože Java zámerne skrýva mnoho aspektov správy pamäte, zistenie, koľko pamäte zaberajú vaše objekty, si vyžaduje určitú prácu. Môžete použiť Runtime.freeMemory () metóda na meranie rozdielov vo veľkosti haldy pred a po pridelení niekoľkých objektov. Niekoľko článkov, napríklad Ramchander Varadarajan „Otázka týždňa č. 107“ (Sun Microsystems, september 2000) a Tony Sintes „Pamäť záleží“ (JavaWorld, Decembra 2001), uveďte túto myšlienku. Bohužiaľ, riešenie bývalého článku zlyháva, pretože implementácia využíva chyby Beh programu metóda, zatiaľ čo riešenie druhého článku má svoje vlastné nedostatky:

  • Jediný hovor na číslo Runtime.freeMemory () sa ukazuje ako nedostatočné, pretože JVM sa môže rozhodnúť kedykoľvek zvýšiť svoju aktuálnu veľkosť haldy (najmä keď prevádzkuje odvoz odpadu). Pokiaľ celková veľkosť haldy už nie je na maximálnej veľkosti -Xmx, mali by sme použiť Runtime.totalMemory () - Runtime.freeMemory () ako použitá veľkosť haldy.
  • Poprava jediného Runtime.gc () hovor nemusí byť dostatočne agresívny na vyžiadanie odvozu odpadu. Mohli by sme napríklad požiadať o spustenie aj finalizátorov objektov. A keďže Runtime.gc () blokovanie nie je zdokumentované, kým sa zhromažďovanie nedokončí, je dobré počkať, kým sa veľkosť vnímanej hromady nestabilizuje.
  • Ak profilovaná trieda vytvorí akékoľvek statické údaje ako súčasť svojej inicializácie triedy pre každú triedu (vrátane statických tried a inicializátorov polí), halda pamäť použitá pre inštanciu prvej triedy môže tieto údaje obsahovať. Mali by sme ignorovať hromadu priestoru spotrebovaného inštanciou prvej triedy.

Vzhľadom na tieto problémy uvádzam Veľkosť, nástroj, s ktorým sa šmýkam na rôznych jadrových a aplikačných triedach Java:

public class Sizeof {public static void main (String [] args) throws Exception {// Zahreje všetky triedy / metódy, ktoré použijeme runGC (); usedMemory (); // Pole na uchovanie silných odkazov na pridelené objekty final int count = 100000; Objekt [] objekty = nový Objekt [počet]; dlhá halda1 = 0; // Prideliť počet + 1 objektov, zahodiť prvý pre (int i = -1; i = 0) objekty [i] = objekt; else {objekt = null; // Zahodiť zahrievací objekt runGC (); halda1 = usedMemory (); // Vytvorte snímku pred haldy}} runGC (); long heap2 = usedMemory (); // Vytvorte snímku po halde: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("" pred 'haldy: "+ heap1 +",' za 'haldy: "+ heap2); System.out.println ("heap delta:" + (heap2 - heap1) + ", {" + objekty [0] .getClass () + "} size =" + size + "bytes"); pre (int i = 0; i <počet; ++ i) objekty [i] = null; objekty = null; } private static void runGC () throws Exception {// Pomáha volať Runtime.gc () // pomocou niekoľkých volaní metód: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () vyvolá výnimku {dlho usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; pre (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 (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec triedy 

Veľkosťkľúčové metódy sú runGC () a usedMemory (). Používam a runGC () wrapper metóda na zavolanie _runGC () niekoľkokrát, pretože sa zdá, že metóda je agresívnejšia. (Nie som si istý, prečo, ale je možné, že vytvorenie a zničenie rámca call-stacku metód spôsobí zmenu v koreňovej sade dosiahnuteľnosti a vyzve zberača odpadu, aby pracoval tvrdšie. Navyše, na vytvorenie dostatku práce spotrebuje veľkú časť haldy. pomáha aj naštartovanie zberača odpadu. Všeobecne je ťažké zabezpečiť, aby sa zhromaždilo všetko. Presné podrobnosti závisia od algoritmu JVM a odvozu odpadu.)

Pozorne si všimnite miesta, kde sa dovolávam runGC (). Môžete upraviť kód medzi halda1 a halda2 vyhlásenia na vytvorenie inštancie všetkého zaujímavého.

Všimnite si tiež ako Veľkosť vypíše veľkosť objektu: prechodné uzavretie dát vyžadované všetkými počítať inštancie triedy delené počítať. Pre väčšinu tried bude výsledkom pamäť spotrebovaná jednou inštanciou triedy vrátane všetkých jej vlastnených polí. Táto hodnota pamäťovej stopy sa líši od údajov poskytnutých mnohými komerčnými profilermi, ktorí hlásia plytké pamäťové stopy (napríklad ak má objekt int [] poľa sa jeho spotreba pamäte zobrazí osobitne).

Výsledky

Použime tento jednoduchý nástroj na niekoľko tried a potom uvidíme, či výsledky zodpovedajú našim očakávaniam.

Poznámka: Nasledujúce výsledky sú založené na softvéri Sun JDK 1.3.1 pre Windows. Z dôvodu toho, čo je a nie je zaručené špecifikáciami jazyka Java a JVM, nemôžete tieto konkrétne výsledky použiť na iné platformy alebo iné implementácie Java.

java.lang.Objekt

No, koreň všetkých objektov musel byť len môj prvý prípad. Pre java.lang.Objekt, Dostávam:

„pred“ hromadou: 510696, „za“ hromadou: 1310696 delta haldy: 800000, {trieda java.lang.Object} veľkosť = 8 bajtov 

Takže, proste Objekt trvá 8 bajtov; samozrejme, nikto by nemal čakať, že veľkosť bude 0, pretože každá inštancia musí nosiť pole, ktoré podporuje základné operácie rovná sa (), hashCode (), čakať () / upozorniť (), a tak ďalej.

java.lang.Integer

Ja a moji kolegovia často balíme natívne ints do Celé číslo inštancie, aby sme ich mohli uložiť do zbierok Java. Koľko nás to stojí v pamäti?

„pred“ hromadou: 510696, „za“ hromadou: 2110696 halda delta: 1600000, {trieda java.lang.Integer} veľkosť = 16 bajtov 

Výsledok 16 bajtov je o niečo horší, ako som čakal, pretože int hodnota sa zmestí len na 4 ďalšie bajty. Pomocou Celé číslo stojí ma to 300 percent režijnej pamäte v porovnaní s tým, keď môžem uložiť hodnotu ako primitívny typ.

java.lang.Dlhé

Dlhé by mali mať viac pamäte ako Celé číslo, ale nie:

„pred“ hromadou: 510696, „za“ hromadou: 2110696 delta haldy: 1600000, {trieda java.lang.Long} veľkosť = 16 bajtov 

Je zrejmé, že skutočná veľkosť objektu na halde podlieha zarovnaniu pamäte na nízkej úrovni vykonanej konkrétnou implementáciou JVM pre konkrétny typ CPU. Vyzerá to ako Dlhé je 8 bajtov Objekt réžia plus 8 bajtov viac za skutočnú dlhú hodnotu. Naproti tomu Celé číslo mal nepoužitý 4-bajtový otvor, najpravdepodobnejšie preto, že JVM, ktorý používam, vynúti zarovnanie objektu na hranici 8-bajtového slova.

Polia

Hra s poľami primitívneho typu sa ukazuje ako poučná, čiastočne pri zisťovaní skrytých réžií a čiastočne pri ospravedlňovaní iného populárneho triku: zabalenia primitívnych hodnôt do poľa veľkosti 1 a ich použitia ako objektov. Úpravou Sizeof.main () mať slučku, ktorá zvyšuje vytvorenú dĺžku poľa pri každej iterácii, chápem pre int polia:

dĺžka: 0, {trieda [I} veľkosť = 16 bajtov dĺžka: 1, {trieda [I} veľkosť = 16 bajtov dĺžka: 2, {trieda [I} veľkosť = 24 bajtov dĺžka: 3, {trieda [I} veľkosť = 24 bajtov dĺžka: 4, {trieda [I} veľkosť = 32 bajtov dĺžka: 5, {trieda [I} veľkosť = 32 bajtov dĺžka: 6, {trieda [I} veľkosť = 40 bajtov dĺžka: 7, {trieda [I} size = 40 bytes length: 8, {class [I} size = 48 bytes length: 9, {class [I} size = 48 bytes length: 10, {class [I} size = 56 bytes 

a pre char polia:

dĺžka: 0, {trieda [C} veľkosť = 16 bajtov dĺžka: 1, {trieda [C} veľkosť = 16 bajtov dĺžka: 2, {trieda [C} veľkosť = 16 bajtov dĺžka: 3, {trieda [C} veľkosť = 24 bajtov dĺžka: 4, {trieda [C} veľkosť = 24 bajtov dĺžka: 5, {trieda [C} veľkosť = 24 bajtov dĺžka: 6, {trieda [C} veľkosť = 24 bajtov dĺžka: 7, {trieda [C} veľkosť = 32 bajtov dĺžka: 8, {trieda [C} veľkosť = 32 bajtov dĺžka: 9, {trieda [C} veľkosť = 32 bajtov dĺžka: 10, {trieda [C} veľkosť = 32 bajtov 

Vyššie sa opäť objavia dôkazy o 8-bajtovom vyrovnaní. Tiež okrem nevyhnutného Objekt 8-bytová réžia, primitívne pole pridáva ďalších 8 bajtov (z toho najmenej 4 bajty podporujú dĺžka lúka). A pomocou int [1] Zdá sa, že oproti Celé číslo inštancia, snáď s výnimkou premenlivej verzie rovnakých údajov.

Multidimenzionálne polia

Multidimenzionálne polia ponúkajú ďalšie prekvapenie. Vývojári bežne používajú konštrukcie ako int [dim1] [dim2] v numerických a vedeckých výpočtoch. V int [dim1] [dim2] inštancia poľa, každá vnorená int [dim2] pole je Objekt sama o sebe. Každý z nich pridáva obvyklú réžiu 16-bajtového poľa. Keď nepotrebujem trojuholníkové alebo členité pole, predstavuje to čistú réžiu. Dopad rastie, keď sa rozmery poľa výrazne líšia. Napríklad a int [128] [2] inštancia trvá 3 600 bajtov. V porovnaní s 1 040 bajtami int [256] inštančné použitie (ktoré má rovnakú kapacitu), 3 600 bajtov predstavuje 246 percent réžie. V extrémnom prípade bajt [256] [1], režijný faktor je takmer 19! Porovnajte to so situáciou C / C ++, v ktorej rovnaká syntax nepridáva žiadnu réžiu úložiska.

java.lang.String

Skúsme prázdno String, najskôr skonštruované ako nový reťazec ():

„pred“ hromadou: 510696, „za“ hromadou: 4510696 delta haldy: 4000000, {trieda java.lang.String} veľkosť = 40 bajtov 

Výsledok je dosť depresívny. Prázdny String zaberá 40 bajtov - dostatok pamäte na 20 znakov Java.

Skôr ako to skúsim Strings obsahom potrebujem na vytvorenie pomocnú metódu Stringje zaručené, že nebude internovaný. Iba použitie literálov ako v:

 object = "reťazec s 20 znakmi"; 

nebude fungovať, pretože všetky také úchyty objektov budú nakoniec smerovať na rovnaké String inštancia. Špecifikácia jazyka diktuje takéto správanie (pozri tiež java.lang.String.intern () metóda). Preto, aby sme pokračovali v sledovaní našej pamäte, skúste:

 public static String createString (final int length) {char [] result = new char [length]; pre (int i = 0; i <dĺžka; ++ i) výsledok [i] = (char) i; vrátiť nový reťazec (výsledok); } 

Po tom, čo som sa tým vyzbrojil String metóda autora, mám nasledujúce výsledky:

dĺžka: 0, {trieda java.lang.String} veľkosť = 40 bajtov dĺžka: 1, {trieda java.lang.String} veľkosť = 40 bajtov dĺžka: 2, {trieda java.lang.String} veľkosť = 40 bajtov dĺžka: 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} veľkosť = 56 bajtov dĺžka: 10, {trieda java.lang.String} veľkosť = 56 bajtov 

Výsledky jasne ukazujú, že a Stringrast pamäte sleduje jeho vnútorné char rast poľa. Avšak String trieda pridáva ďalších 24 bajtov réžie. Za neprázdne String s veľkosťou 10 znakov alebo menej, pridané režijné náklady vo vzťahu k užitočnému užitočnému zaťaženiu (2 bajty pre každý char plus 4 bajty za dĺžku) sa pohybuje od 100 do 400 percent.

Pokuta samozrejme závisí od distribúcie údajov vašej aplikácie. Nejako som tušil, že 10 znakov predstavuje to typické String dĺžka pre rôzne aplikácie. Aby som získal konkrétny údajový bod, vybavil som demo SwingSet2 (úpravou String implementácia triedy priamo), ktorá bola dodaná s JDK 1.3.x na sledovanie dĺžok súboru Strings vytvára. Po niekoľkých minútach hrania s ukážkou ukázal výpis dát asi 180 000 Struny boli inštancované. Ich triedenie do vedierok veľkosti potvrdilo moje očakávania:

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

Máte pravdu, viac ako 50 percent všetkých String dĺžky spadli do vedra 0-10, veľmi horúceho miesta roku String triedna neefektívnosť!

V realite, Strings môžu spotrebovať ešte viac pamäte, ako naznačuje ich dĺžka: Strings vygenerované z StringBuffers (buď výslovne, alebo prostredníctvom spojovacieho operátora „+“) pravdepodobne majú char polia s dĺžkami väčšími ako uvádzané String dĺžky pretože StringBuffers zvyčajne začínajú s kapacitou 16, potom ju zdvojnásobte pridať () operácie. Takže napríklad createString (1) + "" končí a char pole veľkosti 16, nie 2.

Čo urobíme?

„Toto je všetko veľmi dobré, ale nezostáva nám nič iné, ako použiť Strings a ďalšie typy poskytované technológiou Java, však? “Počula som, že sa pýtate. Dozvieme sa to.

Triedy zavinovačiek

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