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žítRuntime.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é dobyRuntime.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
Velikost
klíč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ětiva
S obsahem potřebuji k vytvoření pomocnou metodu Tětiva
je 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ětiva
Rů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ětiva
to 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ětiva
s mohou spotřebovat ještě více paměti, než naznačují jejich délky: Tětiva
vygenerováno z StringBuffer
s (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 StringBuffer
s 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ětiva
s a další typy poskytované Java, že? “Slyšel jsem, že se ptáte. Zjistíme to.