Programování

Java Tip 130: Znáte svou velikost dat?

Nedávno jsem pomohl navrhnout serverovou aplikaci Java, která se podobala databázi v paměti. To znamená, že jsme zaujali design směrem k ukládání tun dat do paměti, abychom zajistili super rychlý výkon dotazů.

Jakmile jsme spustili prototyp, přirozeně jsme se rozhodli profilovat stopu datové paměti poté, co byla analyzována a načtena z disku. Neuspokojivé počáteční výsledky mě však vedly k hledání vysvětlení.

Poznámka: Zdrojový kód tohoto článku si můžete stáhnout ze zdrojů.

Nástroj

Jelikož Java záměrně skrývá mnoho aspektů správy paměti, objevování množství paměti, kterou vaše objekty spotřebovávají, vyžaduje nějakou práci. Můžete použít Runtime.freeMemory () metoda měření rozdílů ve velikosti haldy před a po přidělení několika objektů. Několik článků, například Ramchander Varadarajan „Otázka týdne č. 107“ (Sun Microsystems, září 2000) a Tony Sintes „Paměť záleží“ (JavaWorld, Prosinec 2001), podrobně tuto myšlenku. Bohužel řešení předchozího článku selhalo, protože implementace zaměstnává špatně Runtime metoda, zatímco řešení druhého článku má své vlastní nedokonalosti:

  • Jediný hovor na Runtime.freeMemory () se ukáže jako nedostatečné, protože JVM se může rozhodnout kdykoli zvýšit svou aktuální velikost haldy (zejména když spouští uvolňování paměti). Pokud celková velikost haldy již není na maximální velikosti -Xmx, měli bychom použít Runtime.totalMemory () - Runtime.freeMemory () jako použitá velikost haldy.
  • Provedení jediného Runtime.gc () volání nemusí být dostatečně agresivní pro vyžádání uvolnění paměti. Mohli bychom například požádat o spuštění také finalizátorů objektů. A od té doby Runtime.gc () není dokumentováno k blokování, dokud nedojde k dokončení kolekce, je dobré počkat, dokud se vnímaná velikost haldy nestabilizuje.
  • Pokud profilovaná třída vytvoří jakákoli statická data jako součást své inicializace třídy pro každou třídu (včetně statických tříd a inicializátorů polí), může halda paměť použitá pro první instanci třídy tato data obsahovat. Měli bychom ignorovat prostor haldy spotřebovaný instancí první třídy.

Vzhledem k těmto problémům předkládám Velikost, nástroj, pomocí kterého čmuchám na různých třídách jádra a aplikací Java:

public class Sizeof {public static void main (String [] args) throws Exception {// Warm up all classes / methods we will use runGC (); usedMemory (); // Pole pro udržení silných odkazů na přidělené objekty final int count = 100000; Objekt [] objekty = nový Objekt [počet]; dlouhá halda1 = 0; // Přidělit počet + 1 objektů, zahodit první pro (int i = -1; i = 0) objekty [i] = objekt; else {object = null; // Zahodit zahřívací objekt runGC (); halda1 = usedMemory (); // Pořiďte snímek před haldy}} runGC (); long heap2 = usedMemory (); // Pořiďte snímek po haldě: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("" před 'haldy: "+ heap1 +",' za 'haldy: "+ heap2); System.out.println ("delta haldy:" + (heap2 - heap1) + ", {" + objekty [0] .getClass () + "} size =" + size + "bytes"); pro (int i = 0; i <počet; ++ i) objekty [i] = null; objekty = null; } private static void runGC () throws Exception {// It to call to Runtime.gc () // using several method calls: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () vyvolá výjimku {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 (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Konec třídy 

Velikostklíčové metody jsou runGC () a usedMemory (). Používám a runGC () zaváděcí metoda k volání _runGC () několikrát, protože se zdá, že je metoda agresivnější. (Nejsem si jistý proč, ale je možné vytvoření a zničení rámce call-stacku metody způsobí změnu v kořenové sadě dosažitelnosti a vyzve sběrače odpadu, aby pracoval tvrději. Kromě toho spotřebovává velkou část prostoru haldy, aby vytvořila dostatek práce pomáhá také sběratel odpadků. Obecně je těžké zajistit, aby se vše shromáždilo. Přesné podrobnosti závisí na algoritmu JVM a odvozu odpadu.)

Všimněte si pozorně míst, kde se dovolávám runGC (). Kód můžete upravit mezi halda1 a halda2 prohlášení k instanci čehokoli zajímavého.

Všimněte si také, jak Velikost vypíše velikost objektu: přechodné uzavření dat vyžadované všemi počet instance třídy děleno počet. U většiny tříd bude výsledkem paměť spotřebovaná jednou instancí třídy, včetně všech jejích vlastněných polí. Tato hodnota paměťové stopy se liší od dat poskytovaných mnoha komerčními profilovači, kteří hlásí stopy mělké paměti (například pokud má objekt int [] pole se jeho spotřeba paměti zobrazí samostatně).

Výsledky

Pojďme použít tento jednoduchý nástroj na několik tříd, pak uvidíme, jestli výsledky odpovídají našim očekáváním.

Poznámka: Následující výsledky jsou založeny na Sunu JDK 1.3.1 pro Windows. Vzhledem k tomu, co je a není zaručeno specifikacemi jazyka Java a JVM, nemůžete tyto konkrétní výsledky použít na jiné platformy nebo jiné implementace Java.

java.lang.Object

Kořen všech objektů musel být můj první případ. Pro java.lang.Object, Dostanu:

'before' heap: 510696, 'after' heap: 1310696 heap delta: 800000, {class java.lang.Object} size = 8 bytes 

Prostě Objekt trvá 8 bajtů; samozřejmě by nikdo neměl očekávat, že velikost bude 0, protože každá instance musí nést pole, která podporují základní operace jako se rovná(), hashCode (), počkat () / upozornit (), a tak dále.

java.lang.Integer

Moji kolegové a já často zabalíme domorodce ints do Celé číslo instance, abychom je mohli ukládat do sbírek Java. Kolik nás to stojí v paměti?

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

Výsledek 16 bajtů je o něco horší, než jsem čekal, protože int hodnota se vejde do pouhých 4 bytů navíc. Pomocí Celé číslo stojí mě to 300 procent režie paměti ve srovnání s tím, když můžu uložit hodnotu jako primitivní typ.

java.lang.Long

Dlouho by měl mít více paměti než Celé číslo, ale ne:

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

Je zřejmé, že skutečná velikost objektu na haldě podléhá zarovnání paměti na nízké úrovni provedené konkrétní implementací JVM pro konkrétní typ CPU. Vypadá to jako Dlouho je 8 bajtů Objekt režie plus 8 bajtů více pro skutečnou dlouhou hodnotu. V porovnání, Celé číslo měl nepoužitý 4bajtový otvor, pravděpodobně proto, že JVM používám vynucení zarovnání objektu na hranici 8bajtového slova.

Pole

Hraní s poli primitivního typu se ukazuje jako poučné, jednak objevit jakoukoli skrytou režii a jednak ospravedlnit další populární trik: zabalit primitivní hodnoty do pole velikosti 1 a použít je jako objekty. Úpravou Velikost hlavní () mít smyčku, která zvýší vytvořenou délku pole při každé iteraci, chápu int pole:

délka: 0, {třída [I} velikost = 16 bytů délka: 1, {třída [I} velikost = 16 bytů délka: 2, {třída [I} velikost = 24 bytů délka: 3, {třída [I} velikost = Délka 24 bytů: 4, {třída [I} velikost = 32 bytů délka: 5, {třída [I} velikost = 32 bytů délka: 6, {třída [I} velikost = 40 bytů délka: 7, {třída [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 pro char pole:

délka: 0, {třída [C} velikost = 16 bytů délka: 1, {třída [C} velikost = 16 bytů délka: 2, {třída [C} velikost = 16 bytů délka: 3, {třída [C} velikost = 24 bytů délka: 4, {třída [C} velikost = 24 bytů délka: 5, {třída [C} velikost = 24 bytů délka: 6, {třída [C} velikost = 24 bytů délka: 7, {třída [C} velikost = 32 bytů délka: 8, {třída [C} velikost = 32 bytů délka: 9, {třída [C} velikost = 32 bytů délka: 10, {třída [C} velikost = 32 bytů 

Nahoře se znovu objeví důkazy o 8bajtovém zarovnání. Kromě toho nevyhnutelné Objekt 8bajtová režie přidá primitivní pole dalších 8 bajtů (z toho alespoň 4 bajty podporují délka pole). A pomocí int [1] Zdá se, že nenabízí žádné paměťové výhody oproti Celé číslo instance, s výjimkou možná jako měnitelné verze stejných dat.

Vícedimenzionální pole

Vícerozměrná pole nabízejí další překvapení. Vývojáři běžně používají konstrukce jako int [dim1] [dim2] v numerických a vědeckých výpočtech. V int [dim1] [dim2] instance pole, každá vnořená int [dim2] pole je Objekt v jeho právu. Každý přidává obvyklou režii 16bajtového pole. Když nepotřebuji trojúhelníkové nebo členité pole, představuje to čistou režii. Dopad roste, když se rozměry pole značně liší. Například a int [128] [2] instance trvá 3 600 bajtů. Ve srovnání s 1040 bajty an int [256] instance používá (která má stejnou kapacitu), 3 600 bajtů představuje 246 procent režie. V extrémním případě bajt [256] [1], režijní faktor je téměř 19! Porovnejte to se situací C / C ++, ve které stejná syntaxe nepřidává žádnou režii úložiště.

řetězec java.lang

Zkusme prázdnou Tětiva, nejprve zkonstruován jako nový řetězec ():

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

Výsledek je docela depresivní. Prázdný Tětiva trvá 40 bajtů - dostatek paměti pro 20 znaků Java.

Než to zkusím TětivaS obsahem potřebuji k vytvoření pomocnou metodu Tětivaje zaručeno, že nebude internován. Pouhé použití literálů jako v:

 object = "řetězec s 20 znaky"; 

nebude fungovat, protože všechny takové úchyty objektů budou nakonec ukazovat na stejné Tětiva instance. Specifikace jazyka diktuje takové chování (viz také java.lang.String.intern () metoda). Proto, abychom pokračovali v snoopování paměti, zkuste:

 public static String createString (konečná délka int) {char [] result = new char [length]; pro (int i = 0; i <délka; ++ i) výsledek [i] = (char) i; vrátit nový řetězec (výsledek); } 

Poté, co jsem se tím vyzbrojil Tětiva tvůrce metoda, mám následující výsledky:

délka: 0, {třída java.lang.String} velikost = 40 bytů délka: 1, {třída java.lang.String} velikost = 40 bytů délka: 2, {třída java.lang.String} velikost = 40 bytů délka: 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 bytů délka: 10, {třída java.lang.String} velikost = 56 bytů 

Výsledky jasně ukazují, že a TětivaRůst paměti sleduje jeho vnitřní char růst pole. Nicméně Tětiva třída přidá dalších 24 bajtů režie. Pro neprázdné Tětiva o velikosti 10 znaků nebo méně, přidané režijní náklady ve srovnání s užitečným užitečným zatížením (2 bajty za každý char plus 4 bajty pro délku), pohybuje se od 100 do 400 procent.

Pokuta samozřejmě závisí na distribuci dat vaší aplikace. Nějak jsem měl podezření, že 10 znaků představuje typické Tětiva délka pro různé aplikace. Abych získal konkrétní datový bod, vybavil jsem demo SwingSet2 (úpravou Tětiva přímo implementace třídy), která byla dodána s JDK 1.3.x ke sledování délek souboru Tětivato vytváří. Po několika minutách hraní s ukázkou ukázal výpis dat asi 180 000 Struny byly vytvořeny instance. Třídění do kbelíků velikosti potvrdilo moje očekávání:

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

To je pravda, více než 50 procent všech Tětiva délky spadly do kbelíku 0-10, velmi horkého místa Tětiva neefektivnost třídy!

V realitě, Tětivas mohou spotřebovat ještě více paměti, než naznačují jejich délky: Tětivavygenerováno z StringBuffers (buď výslovně, nebo prostřednictvím operátoru zřetězení '+') pravděpodobně mají char pole s délkami většími než uváděné Tětiva délky protože StringBuffers obvykle začínají s kapacitou 16, pak ji zdvojnásobte připojit() operace. Například createString (1) + '' končí a char pole velikosti 16, ne 2.

Co děláme?

„To je všechno v pořádku, ale nemáme jinou možnost, než použít Tětivas a další typy poskytované Java, že? “Slyšel jsem, že se ptáte. Zjistíme to.

Obalové třídy

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