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 -XmxRuntime.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 odkarRuntime.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
Velikost
Ključ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 Vrvica
s vsebino potrebujem pomožno metodo za ustvarjanje Vrvica
zagotovljeno, 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 Vrvica
Rast 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 Vrvica
s 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 Vrvica
s lahko porabijo celo več pomnilnika, kot kaže njihova dolžina: Vrvica
ustvarjene iz StringBuffer
s (bodisi izrecno bodisi prek operaterja združevanja '+') verjetno char
polja z dolžinami večjimi od prijavljenih Vrvica
dolžine, ker StringBuffer
s 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 Vrvica
in druge vrste, ki jih ponuja Java, kajne? "Slišim, da vprašate. Ugotovimo.