Ohjelmointi

Java-vinkki 130: Tiedätkö tietosi koon?

Viime aikoina autoin suunnittelemaan Java-palvelinsovelluksen, joka muistutti muistin sisäistä tietokantaa. Toisin sanoen suunnittelemme suunnittelua kohti tonnimäärän tietojen tallentamista välimuistiin supernopean kyselyn suorituskyvyn tarjoamiseksi.

Kun prototyyppi oli käynnissä, päätimme luonnollisesti profiloida datamuistin jalanjäljen sen jälkeen, kun se oli jäsennelty ja ladattu levyltä. Epätyydyttävät alkutulokset saivat kuitenkin minut etsimään selityksiä.

merkintä: Voit ladata tämän artikkelin lähdekoodin Resursseista.

Työkalu

Koska Java kätkee tarkoituksenmukaisesti monia muistinhallinnan näkökohtia, objektien kuluttaman muistin selvittäminen vie jonkin verran työtä. Voit käyttää Runtime.freeMemory () menetelmä kasan koon erojen mittaamiseksi ennen ja kun useita esineitä on osoitettu. Useat artikkelit, kuten Ramchander Varadarajanin "Viikon kysymys nro 107" (Sun Microsystems, syyskuu 2000) ja Tony Sintesin "Memory Matters" (JavaWorld, Joulukuussa 2001), yksityiskohtaisesti ajatus. Valitettavasti edellisen artikkelin ratkaisu epäonnistuu, koska toteutus käyttää väärää Ajonaika menetelmä, kun taas jälkimmäisen artikkelin ratkaisulla on omat puutteet:

  • Yksi puhelu numeroon Runtime.freeMemory () osoittautuu riittämättömäksi, koska JVM voi päättää lisätä nykyistä kasan kokoa milloin tahansa (varsinkin kun se suorittaa roskien keräystä). Ellei koko kasan koko ole jo -Xmx maksimikoko, meidän tulisi käyttää Runtime.totalMemory () - Runtime.freeMemory () käytettyä kasan kokoa.
  • Yhden suorittaminen Runtime.gc () puhelu ei välttämättä ole riittävän aggressiivinen roskakorin pyytämistä varten. Voisimme esimerkiksi pyytää objektin viimeistelijöitä suorittamaan myös. Ja siitä lähtien Runtime.gc () ei ole dokumentoitu estettäväksi, ennen kuin keräys on valmis, on hyvä odottaa, kunnes havaittu kasan koko vakiintuu.
  • Jos profiloitu luokka luo staattista dataa osana luokkakohtaista alustustaan ​​(mukaan lukien staattinen luokka ja kenttäalustusohjelmat), ensimmäisessä luokan ilmentymässä käytetty kasa muisti voi sisältää kyseisen tiedon. Meidän tulisi jättää huomiotta ensimmäisen luokan instanssin kuluttama kasatila.

Ottaen huomioon nämä ongelmat, esitän Koko, työkalu, jolla nuuskan erilaisia ​​Java-ydin- ja sovellusluokkia:

public class Size of {public static void main (String [] args) heittää poikkeuksen {// Lämmitä kaikki luokat / menetelmät, joita käytämme runGC (); käytetty muisti (); // Taulukko pitää vahvat viitteet varattuihin kohteisiin lopullinen int-luku = 100000; Object [] objektit = new Object [count]; pitkä kasa1 = 0; // Kohdista määrä + 1 objektia, hylkää ensimmäinen kohteille (int i = -1; i = 0) esineitä [i] = esine; else {objekti = nolla; // Hylkää lämpenemiskohde runGC (); kasa1 = käytetty Muisti (); // Ota tilannekuva ennen kasaa}} runGC (); pitkä kasa2 = käytetty Muisti (); // Ota jälkikasan tilannekuva: lopullinen int-koko = Math.round (((float) (kasa2 - kasa1)) / määrä); System.out.println ("'ennen' kasa:" + kasa1 + ", 'jälkeen kasa:" + kasa2); System.out.println ("kasan delta:" + (kasa2 - kasa1) + ", {" + objektit [0] .getClass () + "} koko =" + koko + "tavua"); for (int i = 0; i <count; ++ i) esineille [i] = null; esineet = nolla; } private static void runGC () heittää poikkeuksen {// Se auttaa soittamaan Runtime.gc () // käyttämällä useita menetelmäkutsuja: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () heittää poikkeuksen {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (käytettyMem1 <käytettyMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); käytettyMem2 = käytettyMem1; käytettyMem1 = käytetty muisti (); }} yksityinen staattinen pitkään käytettyMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } yksityinen staattinen lopullinen ajonaikainen s_runtime = ajonaikainen.getRuntime (); } // Luokan loppu 

KokoTärkeimmät menetelmät ovat runGC () ja käytetty muisti (). Käytän a runGC () kääre menetelmä soittaa _runGC () useita kertoja, koska se näyttää tekevän menetelmästä aggressiivisemman. (En ole varma miksi, mutta on mahdollista, että menetelmän kutsupinokehyksen luominen ja tuhoaminen aiheuttaa muutoksen saavutettavuusjuurijoukossa ja kehottaa roskakorin työskentelemään kovemmin. Lisäksi kuluttaa suuren osan kasatilasta tarpeeksi työtä varten auttaa myös roskien keräilijä. Yleensä on vaikea varmistaa, että kaikki kerätään. Tarkat yksityiskohdat riippuvat JVM: stä ja roskien keräysalgoritmista.)

Huomaa huolellisesti paikat, joihin vetoan runGC (). Voit muokata koodia kasa 1 ja kasa2 julistuksia kiinnostuksen herättämiseksi.

Huomaa myös miten Koko tulostaa objektin koon: kaikkien vaatima tietojen siirtyvä sulkeminen Kreivi luokan esiintymät jaettuna Kreivi. Useimmissa luokissa tulos on yhden luokan esiintymän kuluttama muisti, mukaan lukien kaikki sen omat kentät. Tämä muistin jalanjäljen arvo eroaa monien kaupallisten profiloijien toimittamista tiedoista, jotka ilmoittavat matalan muistin jalanjäljet ​​(esimerkiksi jos objektilla on int [] kenttä, sen muistin kulutus näkyy erikseen).

Tulokset

Sovelletaan tätä yksinkertaista työkalua muutamaan luokkaan ja katsotaan sitten, vastaavatko tulokset odotuksiamme.

merkintä: Seuraavat tulokset perustuvat Sunin JDK 1.3.1 for Windows -käyttöjärjestelmään. Johtuen siitä, mitä Java-kieli ja JVM-määritykset takaavat ja mitä ei voida taata, et voi soveltaa näitä erityistuloksia muihin alustoihin tai muihin Java-toteutuksiin.

java.lang.objekti

No, kaikkien esineiden juuren piti olla vain ensimmäinen tapaukseni. Sillä java.lang.objekti, Saan:

"ennen" kasa: 510696, "jälkeen" kasa: 1310696 kasan delta: 800000, {class java.lang.Object} koko = 8 tavua 

Joten tavallinen Esine vie 8 tavua; tietenkään kenenkään ei pitäisi odottaa koon olevan 0, koska jokaisen esiintymän on kuljettava kenttiä, jotka tukevat perusoperaatioita on yhtä suuri (), hash koodin(), odota () / ilmoita (), ja niin edelleen.

java.lang.I kokonaisluku

Kollegani ja minä kääritään usein alkuperäisiä majat osaksi Kokonaisluku instansseja, jotta voimme tallentaa ne Java-kokoelmiin. Paljonko se maksaa meille muistissa?

'ennen' kasa: 510696, 'jälkeen' kasa: 2110696 kasan delta: 1600000, {class java.lang.Integer} koko = 16 tavua 

16-tavuinen tulos on hieman huonompi kuin odotin, koska int arvo mahtuu vain 4 ylimääräiseen tavuun. Käyttämällä Kokonaisluku maksaa minulle 300 prosenttia muistin yleiskustannuksista verrattuna siihen, kun voin tallentaa arvon primitiivisenä tyypinä.

java.lang.pitkä

Pitkä pitäisi viedä enemmän muistia kuin Kokonaisluku, mutta se ei:

'ennen' kasa: 510696, 'jälkeen' kasa: 2110696 kasan delta: 1600000, {class java.lang.Long} koko = 16 tavua 

Kassan todellinen objektikoko riippuu tietyn JVM-toteutuksen suorittamasta matalan tason muistin tasosta tietylle CPU-tyypille. Se näyttää a Pitkä on 8 tavua Esine plus 8 tavua enemmän todelliselle pitkälle arvolle. Verrattuna, Kokonaisluku oli käyttämätön 4-tavuinen reikä, todennäköisesti siksi, että käyttämäni JVM pakottaa kohteiden kohdistuksen kahdeksan tavun sanarajaan.

Taulukot

Primitiivityyppisillä taulukoilla pelaaminen osoittautuu opettavaiseksi, osittain piilotettujen piirtoheitinten löytämiseksi ja osittain toisen suositun temppun perustelemiseksi: primitiivisten arvojen kääriminen koko-1-matriisiin niiden käyttämiseksi esineinä. Muokkaamalla Mainof Size () saada silmukka, joka lisää luotua taulukon pituutta jokaisella iteraatiolla, saan int taulukot:

pituus: 0, {luokka [I} koko = 16 tavua pituus: 1, {luokka [I} koko = 16 tavua pituus: 2, {luokka [I} koko = 24 tavua pituus: 3, {luokka [I} koko = 24 tavun pituus: 4, {luokka [I} koko = 32 tavua pituus: 5, {luokka [I} koko = 32 tavua pituus: 6, {luokka [I} koko = 40 tavua pituus: 7, {luokka [I}) koko = 40 tavun pituus: 8, {luokka [I} koko = 48 tavua pituus: 9, {luokka [I} koko = 48 tavua pituus: 10, {luokka [I} koko = 56 tavua) 

ja varten hiiltyä taulukot:

pituus: 0, {luokka [C} koko = 16 tavua pituus: 1, {luokka [C} koko = 16 tavua pituus: 2, {luokka [C} koko = 16 tavua pituus: 3, {luokka [C} koko = 24 tavun pituus: 4, {luokan [C} koko = 24 tavun pituus: 5, {luokan [C} koko = 24 tavun pituus: 6, {luokan [C} koko = 24 tavun pituus: 7, {luokka [C}) koko = 32 tavun pituus: 8, {luokka [C} koko = 32 tavun pituus: 9, {luokka [C} koko = 32 tavun pituus: 10, {luokka [C} koko = 32 tavua) 

Yllä oleva näyttö 8-tavun kohdistuksesta ponnahtaa jälleen esiin. Myös väistämättömän lisäksi Esine 8 tavun yläpuolella, primitiivinen taulukko lisää vielä 8 tavua (joista vähintään 4 tavua tukee pituus ala). Ja käyttämällä int [1] ei näytä tarjoavan muistin etuja verrattuna Kokonaisluku esimerkiksi paitsi saman tiedon muutettavana versiona.

Moniulotteiset taulukot

Moniulotteiset taulukot tarjoavat toisen yllätyksen. Kehittäjät käyttävät yleensä sellaisia ​​rakenteita int [dim1] [dim2] numeerisessa ja tieteellisessä laskennassa. Vuonna int [dim1] [dim2] taulukkoesimerkki, jokainen sisäkkäin int [himmeä] taulukko on Esine itsessään. Jokainen lisää tavallisen 16-tavun matriisin yleiskustannukset. Kun en tarvitse kolmionmuotoista tai repaleista ryhmää, se edustaa puhdasta yläpuolta. Vaikutus kasvaa, kun matriisin mitat eroavat suuresti. Esimerkiksi a int [128] [2] esimerkki vie 3600 tavua. Verrattuna 1040 tavuun an int [256] esimerkkikäytöt (joilla on sama kapasiteetti), 3600 tavua edustavat 246 prosentin yleiskustannuksia. Äärimmäisissä tapauksissa tavu [256] [1], yleiskerroin on melkein 19! Vertaa sitä C / C ++ -tilanteeseen, jossa sama syntakse ei lisää tallennustilan yleiskustannuksia.

java.lang.String

Yritetään tyhjää Merkkijono, rakennettu ensin nimellä uusi merkkijono ():

"ennen" kasa: 510696, "jälkeen" kasa: 4510696 kasan delta: 4000000, {class java.lang.String} koko = 40 tavua 

Tulos osoittautuu melko masentavaksi. Tyhjä Merkkijono vie 40 tavua - tarpeeksi muistia 20 Java-merkin sijoittamiseen.

Ennen kuin yritän MerkkijonoSisältöä tarvitsen auttaja-menetelmän luomiseen Merkkijonos ei taatusti internoitavan. Pelkkä kirjaimien käyttö kuten:

 object = "merkkijono, jossa on 20 merkkiä"; 

ei toimi, koska kaikki tällaiset objektikahvat osoittavat samaa Merkkijono ilmentymä. Kielimäärittely sanelee tällaisen käyttäytymisen (katso myös java.lang.String.intern () menetelmä). Siksi jatkaaksesi muistin sieppaamista, yritä:

 julkinen staattinen merkkijono createString (lopullinen int pituus) {char [] tulos = uusi char [pituus]; for (int i = 0; i <pituus; ++ i) tulos [i] = (char) i; palauta uusi merkkijono (tulos); } 

Aseistettuani itseni tällä Merkkijono luoja-menetelmä, saan seuraavat tulokset:

pituus: 0, {luokan java.lang.String} koko = 40 tavun pituus: 1, {luokan java.lang.String} koko = 40 tavun pituus: 2, {luokan java.lang.String} koko = 40 tavun pituus: 3, {luokan java.lang.String} koko = 48 tavun pituus: 4, {luokan java.lang.String} koko = 48 tavun pituus: 5, {luokan java.lang.String} koko = 48 tavun pituus: 6, {luokan java.lang.String} koko = 48 tavun pituus: 7, {luokan java.lang.String} koko = 56 tavun pituus: 8, {luokan java.lang.String} koko = 56 tavun pituus: 9, {luokka java.lang.String} koko = 56 tavun pituus: 10, {luokan java.lang.String} koko = 56 tavua 

Tulokset osoittavat selvästi, että a Merkkijonomuistin kasvu seuraa sen sisäistä hiiltyä taulukon kasvu. Kuitenkin Merkkijono luokka lisää vielä 24 tavua yleiskustannuksia. Sillä ei Merkkijono 10 merkkiä tai vähemmän, lisätyt yleiskustannukset suhteessa hyötykuormaan (2 tavua kullekin hiiltyä plus 4 tavua pituudelle), vaihtelee 100-400 prosenttia.

Tietysti rangaistus riippuu sovelluksesi tietojen jakelusta. Jotenkin epäilin, että 10 merkkiä edustaa tyypillistä Merkkijono pituus erilaisiin sovelluksiin. Saadaksesi konkreettisen datapisteen, instrumentoin SwingSet2-demon (muokkaamalla Merkkijono luokan toteutus suoraan), joka toimitettiin JDK 1.3.x: n mukana seuraamaan Merkkijonos se luo. Muutaman minuutin demon kanssa pelaamisen jälkeen data dump osoitti, että noin 180 000 Jouset välitettiin. Lajittelu koon ämpäreihin vahvisti odotukseni:

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

Aivan, yli 50 prosenttia kaikista Merkkijono pituudet putosivat 0-10 ämpäriin, erittäin kuumaan pisteeseen Merkkijono luokan tehottomuus!

Todellisuudessa, Merkkijonos voi kuluttaa jopa enemmän muistia kuin niiden pituudet viittaavat: Merkkijonos syntynyt StringBuffers (joko nimenomaisesti tai ketjutusoperaattorin kautta) todennäköisesti hiiltyä taulukot, joiden pituudet ovat suuremmat kuin ilmoitetut Merkkijono pituudet, koska StringBufferNe alkavat tyypillisesti 16 kapasiteetilla, sitten kaksinkertaistavat sen liitä () toimintaan. Joten esimerkiksi createString (1) + '' loppuu a hiiltyä koko 16, ei 2.

Mitä me teemme?

"Kaikki on hyvin, mutta meillä ei ole muuta vaihtoehtoa kuin käyttää Merkkijonos ja muut Java: n tarjoamat tyypit, eikö niin? "Kuulen sinun kysyvän. Selvitetään.

Käärintäluokat

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