Programiranje

Nasvet Java 130: Ali poznate svojo velikost podatkov?

Pred kratkim sem pomagal oblikovati strežniško aplikacijo Java, ki je bila podobna podatkovni bazi v pomnilniku. To pomeni, da smo zasnovo usmerili k predpomnjenju ton podatkov v pomnilniku, da bi zagotovili izjemno hitro izvedbo poizvedb.

Ko smo prototip zagnali, smo se po razčlenjevanju in nalaganju z diska seveda odločili za profil odtisa podatkovnega pomnilnika. Nezadovoljivi začetni rezultati pa so me spodbudili k iskanju razlag.

Opomba: Izvorno kodo tega članka lahko prenesete iz virov.

Orodje

Ker Java namenoma skriva številne vidike upravljanja pomnilnika, odkrivanje, koliko pomnilnika porabijo vaši predmeti, zahteva nekaj dela. Lahko uporabite Runtime.freeMemory () metoda za merjenje razlik v velikosti kopice pred in po dodelitvi več predmetov. Številni članki, kot sta Ramchander Varadarajan "Vprašanje tedna št. 107" (Sun Microsystems, september 2000) in Tony Sintes "Memory Matters" (JavaWorld, Decembra 2001), podrobno predstavite to idejo. Na žalost rešitev prejšnjega članka ne uspe, ker izvedba uporablja napako Izvajanje metoda, medtem ko ima rešitev slednjega članka svoje pomanjkljivosti:

  • En sam klic Runtime.freeMemory () se izkaže za nezadostno, ker se lahko JVM kadar koli odloči za povečanje trenutne velikosti kopice (zlasti kadar izvaja zbiranje smeti). Uporabiti moramo, če skupna velikost kopice že ni največja -Xmx Runtime.totalMemory () - Runtime.freeMemory () kot uporabljena velikost kopice.
  • Izvedba enega samega Runtime.gc () klic se morda ne bo izkazal za dovolj agresiven za zahtevanje odvoza smeti. Lahko bi na primer zahtevali, da se zaženejo tudi zaključevalniki predmetov. In odkar Runtime.gc () ni dokumentirano blokirati, dokler se zbiranje ne zaključi, je dobro počakati, da se zaznana velikost kupa stabilizira.
  • Če profilirani razred ustvari kakršne koli statične podatke kot del inicializacije razreda po razredu (vključno s statičnimi inicializatorji razredov in polj), lahko pomnilnik kupa, uporabljen za primerek prvega razreda, vključuje te podatke. Ne smemo zanemariti prostora kopice, ki ga je zasedel primerek prvega razreda.

Glede na te težave predstavljam Velikost, orodje, s katerim opazujem različne razrede jedra Java in aplikacije:

javni razred Sizeof {public static void main (String [] args) vrže izjemo {// Ogrejte vse razrede / metode, ki jih bomo uporabili runGC (); usedMemory (); // Matrika za ohranjanje močnih referenc na dodeljene predmete končno int count = 100000; Objekt [] predmeti = nov objekt [štetje]; dolga kopica1 = 0; // Dodelimo število + 1 predmetov, prvega zavržemo za (int i = -1; i = 0) predmete [i] = objekt; else {objekt = null; // zavržemo predmet ogrevanja runGC (); heap1 = usedMemory (); // Naredi posnetek pred kupom}} runGC (); dolg kup2 = usedMemory (); // Naredi posnetek posnetka po kopici: končna velikost int = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'pred' kopico:" + kopica1 + ", 'po' kopica:" + kopica2); System.out.println ("delnica kopice:" + (kopica2 - kopica1) + ", {" + predmeti [0] .getClass () + "} velikost =" + velikost + "bajtov"); za (int i = 0; i <count; ++ i) predmeti [i] = null; predmeti = null; } private static void runGC () vrže izjemo {// Pomaga poklicati Runtime.gc () // z uporabo več klicev metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () vrže izjemo {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 (); }} zasebni statični long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } zasebni statični končni čas izvajanja s_runtime = Runtime.getRuntime (); } // Konec predavanja 

VelikostKljučne metode so runGC () in usedMemory (). Uporabljam a runGC () zavijalna metoda za klic _runGC () večkrat, ker se zdi, da je metoda bolj agresivna. (Nisem prepričan, zakaj, toda možno je, da ustvarjanje in uničenje okvira metode klicev metode povzroči spremembo nabora korenov dosegljivosti in spodbudi zbiralnik smeti, da dela težje. Poleg tega poraba velikega dela prostora kopice ustvari dovolj dela pomaga tudi zbiralnik smeti. Na splošno je težko zagotoviti, da se vse zbere. Natančne podrobnosti so odvisne od JVM in algoritma za odvoz smeti.)

Pazljivo si oglejte mesta, na katere se sklicujem runGC (). Kodo lahko urejate med kopica1 in kup2 deklaracije za primer vsega, kar vas zanima.

Upoštevajte tudi, kako Velikost natisne velikost predmeta: prehodno zapiranje podatkov, ki ga zahtevajo vsi štetje primerki razreda, deljeni z štetje. Za večino razredov bo rezultat zasedel en primerek razreda, vključno z vsemi polji v lasti. Ta vrednost odtisa pomnilnika se razlikuje od podatkov številnih komercialnih profilov, ki poročajo o majhnih odtisih pomnilnika (na primer, če ima predmet int [] polje za porabo pomnilnika prikaže ločeno).

Rezultati

Uporabimo to preprosto orodje za nekaj razredov, nato pa preverimo, ali rezultati ustrezajo našim pričakovanjem.

Opomba: Naslednji rezultati temeljijo na Sun-ovem JDK 1.3.1 za Windows. Zaradi tega, kar jezik Java in specifikacije JVM ne jamčijo, teh posebnih rezultatov ne morete uporabiti na drugih platformah ali drugih izvedbah Java.

java.lang.Object

No, koren vseh predmetov je moral biti moj prvi primer. Za java.lang.Object, Dobim:

'before' heap: 510696, 'after' heap: 1310696 delta kopice: 800000, {class java.lang.Object} velikost = 8 bajtov 

Torej, navaden Predmet zavzame 8 bajtov; Seveda nihče ne sme pričakovati, da bo velikost 0, saj mora vsak primerek nositi polja, ki podpirajo osnovne operacije enako (), hashCode (), počakaj () / obvesti (), in tako naprej.

java.lang.Integer

S sodelavci pogosto zavijamo domače ints v Celo število primerke, da jih lahko shranimo v zbirke Java. Koliko nas stane v spominu?

'before' heap: 510696, 'after' heap: 2110696 delta kopice: 1600000, {class java.lang.Integer} velikost = 16 bajtov 

Rezultat 16 bajtov je nekoliko slabši, kot sem pričakoval, ker je int vrednost se lahko prilega samo 4 dodatnim bajtom. Uporaba Celo število stane 300-odstotni režijski pomnilnik v primerjavi s časom, ko lahko vrednost shranim kot primitiven tip.

java.lang.Long

dolga bi moral vzeti več pomnilnika kot Celo število, vendar ne:

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

Jasno je, da je dejanska velikost predmeta na kupu odvisna od poravnave pomnilnika na nizki ravni, ki jo izvede določena izvedba JVM za določeno vrsto CPU. Videti je kot dolga je 8 bajtov Predmet režijski stroški, plus 8 bajtov več za dejansko dolgo vrednost. V nasprotju, Celo število je imel neizkoriščeno 4-bajtno luknjo, najverjetneje zato, ker JVM, ki ga uporabljam, vsiljuje poravnavo predmeta na 8-bajtni meji besede.

Polja

Igranje s polji primitivnega tipa se izkaže za poučno, deloma za odkrivanje kakršnih koli skritih režij in deloma za utemeljitev še enega priljubljenega trika: zavijanje primitivnih vrednosti v polje velikosti 1, da jih uporabimo kot predmete. S spreminjanjem Sizeof.main () da imam zanko, ki povečuje ustvarjeno dolžino polja na vsaki ponovitvi, dobim za int nizi:

dolžina: 0, {class [I} size = 16 bytes length: 1, {class [I} size = 16 bytes length: 2, {class [I} size = 24 bytes length: 3, {class [I} size = Dolžina 24 bajtov: 4, {class [I} size = 32 bytes length: 5, {class [I} size = 32 bytes length: 6, {class [I} size = 40 bytes length: 7, {class [I} velikost = 40 bajtov dolžina: 8, {razred [I} velikost = 48 bajtov dolžina: 9, {razred [I} velikost = 48 bajtov dolžina: 10, {razred [I} velikost = 56 bajtov 

in za char nizi:

dolžina: 0, {class [C} size = 16 bytes length: 1, {class [C} size = 16 bytes length: 2, {class [C} size = 16 bytes length: 3, {class [C} size = Dolžina 24 bajtov: 4, {class [C} size = 24 bytes length: 5, {class [C} size = 24 bytes length: 6, {class [C} size = 24 bytes length: 7, {class [C} velikost = 32 bajtov dolžina: 8, {razred [C} velikost = 32 bajtov dolžina: 9, {razred [C} velikost = 32 bajtov dolžina: 10, {razred [C} velikost = 32 bajtov 

Zgoraj se spet pojavijo dokazi o 8-bajtni poravnavi. Tudi poleg neizogibnega Predmet 8-bajtni režijski stroški primitivno polje doda še 8 bajtov (od tega vsaj 4 bajti podpirajo dolžina polje). In uporabo int [1] Zdi se, da ne ponuja nobenih pomnilniških prednosti pred Celo število primer, razen morda kot spremenljiva različica istih podatkov.

Večdimenzionalni nizi

Večdimenzionalni nizi ponujajo še eno presenečenje. Razvijalci pogosto uporabljajo takšne konstrukcije int [dim1] [dim2] v numeričnem in znanstvenem računalništvu. V int [dim1] [dim2] primerek polja, vsak ugnezdeni int [dim2] matrika je Predmet samo po sebi. Vsak doda običajne 16-bajtne matrične režijske stroške. Ko ne potrebujem trikotne ali raztrgane matrike, to predstavlja čisto režijo. Vpliv narašča, ko se dimenzije polja močno razlikujejo. Na primer, a int [128] [2] primerek zavzame 3.600 bajtov. V primerjavi z 1.040 bajti an int [256] Primer uporabe (ki ima enako zmogljivost), 3.600 bajtov predstavlja 246-odstotni režijski strošek. V skrajnem primeru bajt [256] [1], režijski faktor je skoraj 19! Primerjajte to s situacijo C / C ++, v kateri ista sintaksa ne dodaja nobenih dodatnih stroškov.

java.lang.String

Poskusimo prazno Vrvica, najprej zgrajena kot nov niz ():

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

Rezultat se izkaže za precej depresiven. Prazno Vrvica traja 40 bajtov - dovolj pomnilnika, da se prilega 20 znakom Java.

Preden poskusim Vrvicas vsebino potrebujem pomožno metodo za ustvarjanje Vrvicazagotovljeno, da ne bodo internirani. Zgolj z uporabo literal kot v:

 object = "niz z 20 znaki"; 

ne bo delovalo, ker bodo vsi taki ročaji predmetov na koncu pokazali na isto Vrvica primer. Specifikacija jezika narekuje takšno vedenje (glej tudi java.lang.String.intern () metoda). Če želite nadaljevati z brskanjem po spominu, poskusite:

 javni statični niz createString (končna dolžina int) {char [] rezultat = nov znak [dolžina]; za (int i = 0; i <dolžina; ++ i) rezultat [i] = (char) i; vrni nov niz (rezultat); } 

Potem ko sem se oborožil s tem Vrvica ustvarjalca, dobim naslednje rezultate:

dolžina: 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} velikost = 56 bajtov dolžina: 10, {razred java.lang.String} velikost = 56 bajtov 

Rezultati jasno kažejo, da a VrvicaRast spomina sledi notranjemu char rast matrike. Vendar pa Vrvica razred doda še 24 bajtov režijskih stroškov. Za neprazne Vrvica velikosti 10 znakov ali manj, dodani splošni stroški glede na koristno korist (2 bajta za vsakega char plus 4 bajti za dolžino), je od 100 do 400 odstotkov.

Kazen je seveda odvisna od distribucije podatkov vaše aplikacije. Nekako sem sumil, da 10 znakov predstavlja tipično Vrvica dolžina za različne aplikacije. Da bi dobili natančno podatkovno točko, sem pripravil predstavitev SwingSet2 (s spreminjanjem Vrvica izvedba razreda), ki je prišel z JDK 1.3.x za sledenje dolžin Vrvicas ustvarja. Po nekaj minutah igranja z predstavitvijo je izpis podatkov pokazal približno 180.000 Strune bili primerki. Razvrščanje po velikostih je potrdilo moja pričakovanja:

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

Tako je, več kot 50 odstotkov vseh Vrvica dolžine padel v 0-10 vedro, zelo vroča točka Vrvica razredna neučinkovitost!

V resnici pa Vrvicas lahko porabijo celo več pomnilnika, kot kaže njihova dolžina: Vrvicaustvarjene iz StringBuffers (bodisi izrecno bodisi prek operaterja združevanja '+') verjetno char polja z dolžinami večjimi od prijavljenih Vrvica dolžine, ker StringBuffers se običajno začne s kapaciteto 16, nato pa podvoji dodaj () operacij. Tako na primer createString (1) + " konča z a char polje velikosti 16, ne 2.

Kaj počnemo?

"To je vse v redu, vendar nam ne preostane drugega, kot da uporabimo Vrvicain druge vrste, ki jih ponuja Java, kajne? "Slišim, da vprašate. Ugotovimo.

Razredi zavijanja

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