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ähtienRuntime.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
Koko
Tä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 Merkkijono
Sisältöä tarvitsen auttaja-menetelmän luomiseen Merkkijono
s 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 Merkkijono
muistin 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 Merkkijono
s 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, Merkkijono
s voi kuluttaa jopa enemmän muistia kuin niiden pituudet viittaavat: Merkkijono
s syntynyt StringBuffer
s (joko nimenomaisesti tai ketjutusoperaattorin kautta) todennäköisesti hiiltyä
taulukot, joiden pituudet ovat suuremmat kuin ilmoitetut Merkkijono
pituudet, koska StringBuffer
Ne 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ää Merkkijono
s ja muut Java: n tarjoamat tyypit, eikö niin? "Kuulen sinun kysyvän. Selvitetään.