Java savjet 130: Znate li veličinu podataka?

Nedavno sam pomogao u dizajniranju Java poslužiteljske aplikacije koja je nalikovala bazi podataka u memoriji. Odnosno, pristranili smo dizajn prema predmemoriranju tona podataka u memoriji kako bismo pružili super brzu izvedbu upita.

Jednom kad smo pokrenuli prototip, prirodno smo odlučili profilirati otisak memorije podataka nakon što je raščlanjen i učitan s diska. Nezadovoljavajući početni rezultati potaknuli su me na traženje objašnjenja.

Napomena: Izvorni kôd ovog članka možete preuzeti iz izvora.

Alat

Budući da Java namjerno skriva mnoge aspekte upravljanja memorijom, otkrivanje koliko memorije vaši objekti troše zahtijeva malo posla. Runtime.freeMemory()Metodu možete koristiti za mjerenje razlika u veličini hrpe prije i nakon dodjele nekoliko objekata. Nekoliko članaka, poput "Pitanja tjedna br. 107" Ramchandera Varadarajana (Sun Microsystems, rujan 2000.) i Tonyja Sintesa "Memory Matters" ( JavaWorld, prosinac 2001.), detaljno opisuje tu ideju. Nažalost, rješenje prvog članka ne uspijeva jer implementacija koristi pogrešnu Runtimemetodu, dok rješenje drugog članka ima svoje nedostatke:

  • Jedan poziv na Runtime.freeMemory()pokazuje se nedovoljnim jer JVM može odlučiti povećati svoju trenutnu veličinu hrpe u bilo kojem trenutku (posebno kada pokreće skupljanje smeća). Ako ukupna veličina hrpe već nije na -Xmx maksimalnoj veličini, trebali bismo koristiti Runtime.totalMemory()-Runtime.freeMemory()kao korištenu veličinu hrpe.
  • Izvršenje jednog Runtime.gc()poziva možda se neće pokazati dovoljno agresivnim za traženje odvoza smeća. Na primjer, mogli bismo zatražiti da se izvrše i finalizatori objekata. A budući Runtime.gc()da nije dokumentirano da se blokira dok se prikupljanje ne završi, dobra je ideja pričekati dok se opažena veličina hrpe ne stabilizira.
  • Ako profilirana klasa stvori bilo kakve statičke podatke kao dio inicijalizacije klase po klasi (uključujući statičke inicijatore klase i polja), memorija hrpe koja se koristi za prvu instancu klase može sadržavati te podatke. Trebali bismo zanemariti hrpu prostora koju je potrošila instanca prve klase.

Uzimajući u obzir te probleme, predstavljam Sizeofalat s kojim nadgledam razne Java klasne klase i programe:

javna klasa Sizeof {public static void main (String [] args) baca iznimku {// Zagrijte sve klase / metode koje ćemo koristiti runGC (); usedMemory (); // Niz za zadržavanje jakih referenci na dodijeljene objekte final int count = 100000; Objekt [] objekti = novi objekt [count]; duga hrpa1 = 0; // Dodijeliti count + 1 objekata, odbaciti prvi za (int i = -1; i = 0) objekte [i] = object; else {objekt = null; // Odbaci objekt za zagrijavanje runGC (); hrpa1 = usedMemory (); // Napravite snimku prije gomile}} runGC (); duga hrpa2 = usedMemory (); // Napravite snimku nakon hrpe: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'prije' hrpe:" + hrpa1 + ", 'nakon' hrpa:" + hrpa2); System.out.println ("delta hrpe:" + (heap2 - heap1) + ", {" + objekti [0].getClass () + "} size =" + size + "bajtovi"); za (int i = 0; i <count; ++ i) objekte [i] = null; predmeti = null; } private static void runGC () baca iznimku {// Pomaže pozivati ​​Runtime.gc () // koristeći nekoliko poziva metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () baca iznimku {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 (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastavei <brojati; ++ i) objekti [i] = null; predmeti = null; } private static void runGC () baca iznimku {// Pomaže pozivati ​​Runtime.gc () // koristeći nekoliko poziva metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () baca iznimku {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 (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastavei <brojati; ++ i) objekti [i] = null; predmeti = null; } private static void runGC () baca iznimku {// Pomaže pozivati ​​Runtime.gc () // koristeći nekoliko poziva metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () baca iznimku {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 (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastavegc () // koristeći nekoliko poziva metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () baca iznimku {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 (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastavegc () // koristeći nekoliko poziva metode: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () baca iznimku {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 (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastaveThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastaveThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} privatna statička long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } privatno statičko završno vrijeme izvođenja s_runtime = Runtime.getRuntime (); } // Kraj nastave

SizeofKljučne metode su runGC()i usedMemory(). Koristim runGC()metodu omotavanja za pozivanje _runGC()nekoliko puta, jer čini se da je metoda agresivnija. (Nisam siguran zašto, ali moguće je stvaranje i uništavanje okvira call-stacka metode uzrokuje promjenu u korijenskom skupu dostupnosti i potiče sakupljač smeća da radi više. Štoviše, trošenje velikog dijela prostora hrpe za stvaranje dovoljno posla pomaže i sakupljač smeća. Općenito je teško osigurati da se sve skupi. Točni detalji ovise o JVM-u i algoritmu odvoza smeća.)

Pažljivo zabilježite mjesta na koja se pozivam runGC(). Možete urediti kôd između heap1te heap2izjave instantiate bilo interesa.

Također imajte na umu kako Sizeofispisuje veličinu objekta: prijelazno zatvaranje podataka koje zahtijevaju sve countinstance klase, podijeljeno sa count. Za većinu klasa rezultat će zauzeti jedna instanca klase, uključujući sva polja u njezinom vlasništvu. Ta se vrijednost memorijskog otiska razlikuje od podataka mnogih komercijalnih profila koji izvještavaju o površinskim memorijskim otiscima (na primjer, ako objekt ima int[]polje, njegova će se potrošnja memorije prikazati odvojeno).

Rezultati

Primijenimo ovaj jednostavni alat na nekoliko klasa, a zatim provjerimo odgovaraju li rezultati našim očekivanjima.

Napomena: Sljedeći se rezultati temelje na Sunovu JDK 1.3.1 za Windows. Zbog onoga što jest i što nije zajamčeno Java jezikom i JVM specifikacijama, ove određene rezultate ne možete primijeniti na druge platforme ili druge Java implementacije.

java.lang.Object

Pa, korijen svih predmeta jednostavno je morao biti moj prvi slučaj. Jer java.lang.Object, dobivam:

'prije' hrpe: 510696, 'nakon' hrpe: 1310696 hrpa delta: 800000, {class java.lang.Object} size = 8 bajtova 

Dakle, običan Objecttraje 8 bajtova; Naravno, nitko ne bi trebao očekivati veličina biti 0, jer svaki slučaj mora nositi okolo polja koja podrška baza operacija sviđa equals(), hashCode(), wait()/notify(), i tako dalje.

java.lang.Integer

Moje kolege i ja često umotavamo izvorne datoteke intsu Integerinstance kako bismo ih mogli pohraniti u Java kolekcije. Koliko nas košta u sjećanju?

'prije' heap: 510696, 'after' heap: 2110696 heap delta: 1600000, {class java.lang.Integer} size = 16 bajtova 

Rezultat od 16 bajtova malo je lošiji nego što sam očekivao jer intvrijednost može stati u samo 4 dodatna bajta. Korištenje Integerme košta 300 posto memorijskih troškova u usporedbi s tim kada mogu vrijednost pohraniti kao primitivni tip.

java.lang.Dugo

Longtrebao bi uzeti više memorije Integer, ali ne uzima :

'prije' hrpe: 510696, 'poslije' hrpe: 2110696 hrpa delta: 1600000, {class java.lang.Long} size = 16 bajtova 

Jasno je da je stvarna veličina objekta na hrpi podložna poravnanju memorije na niskoj razini koje vrši određena JVM implementacija za određeni tip CPU-a. Izgleda da Longje a 8 bajta Objectrežijskih troškova, plus 8 bajta više za stvarnu dugu vrijednost. Suprotno tome, Integerimao je neiskorištenu rupu od 4 bajta, najvjerojatnije zato što JVM koji koristim prisiljava poravnanje objekta na 8-bajtnoj granici riječi.

Nizovi

Igranje s nizovima primitivnog tipa pokazuje se poučnim, dijelom za otkrivanje bilo kakvih skrivenih dodatnih troškova, a dijelom za opravdanje još jednog popularnog trika: zamatanje primitivnih vrijednosti u niz veličine 1 kako bi ih se koristilo kao objekte. Izmjenom Sizeof.main()da imam petlju koja povećava duljinu stvorenog polja na svakoj iteraciji, dobivam za intnizove:

duljina: 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 = Duljina 24 bajta: 4, {class [I} size = 32 bytes length: 5, {class [I} size = 32 bytes length: 6, {class [I} size = 40 bytes length: 7, {class [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 za charnizove:

duljina: 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 = Duljina 24 bajta: 4, {class [C} size = 24 bytes length: 5, {class [C} size = 24 bytes length: 6, {class [C} size = 24 bytes length: 7, {class [C} size = 32 bytes length: 8, {class [C} size = 32 bytes length: 9, {class [C} size = 32 bytes length: 10, {class [C} size = 32 bytes 

Iznad se ponovno pojavljuju dokazi o poravnanju od 8 bajtova. Također, uz neizbježne Object8-bajtne režijske troškove, primitivni niz dodaje još 8 bajtova (od kojih najmanje 4 bajta podržavaju lengthpolje). int[1]Čini se da upotreba ne nudi nikakve memorijske prednosti u odnosu na Integerprimjerak, osim možda kao promjenjive verzije istih podataka.

Višedimenzionalni nizovi

Multidimenzionalni nizovi nude još jedno iznenađenje. Programeri obično koriste konstrukcije poput int[dim1][dim2]numeričkog i znanstvenog računanja. U int[dim1][dim2]instanci niza, svaki ugniježđeni int[dim2]niz sam je Objectpo sebi. Svaki dodaje uobičajeni 16-bajtni niz iznad glave. Kad mi ne treba trokutasti ili raščupani niz, to predstavlja čiste troškove. Utjecaj raste kada se dimenzije niza uvelike razlikuju. Na primjer, int[128][2]instanca traje 3.600 bajtova. U usporedbi s 1.040 bajtova koje int[256]instanca koristi (koji imaju isti kapacitet), 3.600 bajtova predstavlja 246 posto općih troškova. U krajnjem slučaju byte[256][1], režijski faktor je gotovo 19! Usporedite to sa situacijom C / C ++ u kojoj ista sintaksa ne dodaje dodatne troškove.

java.lang.String

Let's try an empty String, first constructed as new String():

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

The result proves quite depressing. An empty String takes 40 bytes—enough memory to fit 20 Java characters.

Before I try Strings with content, I need a helper method to create Strings guaranteed not to get interned. Merely using literals as in:

 object = "string with 20 chars"; 

will not work because all such object handles will end up pointing to the same String instance. The language specification dictates such behavior (see also the java.lang.String.intern() method). Therefore, to continue our memory snooping, try:

 public static String createString (final int length) { char [] result = new char [length]; for (int i = 0; i < length; ++ i) result [i] = (char) i; return new String (result); } 

After arming myself with this String creator method, I get the following results:

length: 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} size = 56 bytes length: 10, {class java.lang.String} size = 56 bytes 

The results clearly show that a String's memory growth tracks its internal char array's growth. However, the String class adds another 24 bytes of overhead. For a nonempty String of size 10 characters or less, the added overhead cost relative to useful payload (2 bytes for each char plus 4 bytes for the length), ranges from 100 to 400 percent.

Of course, the penalty depends on your application's data distribution. Somehow I suspected that 10 characters represents the typical String length for a variety of applications. To get a concrete data point, I instrumented the SwingSet2 demo (by modifying the String class implementation directly) that came with JDK 1.3.x to track the lengths of the Strings it creates. After a few minutes playing with the demo, a data dump showed that about 180,000 Strings were instantiated. Sorting them into size buckets confirmed my expectations:

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

That's right, more than 50 percent of all String lengths fell into the 0-10 bucket, the very hot spot of String class inefficiency!

U stvarnosti, Strings mogu potrošiti čak i više memorije nego što njihove duljine sugeriraju: Strings generirane iz StringBuffers (bilo izričito bilo putem operatora spajanja '+') vjerojatno imaju charnizove duljine veće od prijavljenih Stringduljina, jer StringBuffers obično počinju s kapacitetom 16 , a zatim ga udvostručite na append()operacijama. Tako, na primjer, createString(1) + ' 'završava s charnizom veličine 16, a ne 2.

Što nam je činiti?

"Ovo je sve u redu, ali nemamo drugog izbora nego koristiti Strings i druge tipove koje nudi Java, zar ne?" Čujem kako pitaš. Hajde da vidimo.

Satovi omota