Java съвет 130: Знаете ли размера на данните си?

Наскоро помогнах при проектирането на Java сървърно приложение, което приличаше на база данни в паметта. Тоест, сме пристрастили дизайна към кеширане на тонове данни в паметта, за да осигурим супер бърза производителност на заявките.

След като стартирахме прототипа, естествено решихме да профилираме отпечатъка на паметта за данни, след като той беше анализиран и зареден от диска. Незадоволителните първоначални резултати обаче ме подтикнаха да търся обяснения.

Забележка: Можете да изтеглите изходния код на тази статия от ресурси.

Инструментът

Тъй като Java целенасочено крие много аспекти на управлението на паметта, откриването на това колко памет консумират вашите обекти отнема известно време. Можете да използвате Runtime.freeMemory()метода за измерване на разликите в размера на купчината преди и след разпределянето на няколко обекта. Няколко статии, като „Въпросът на седмицата № 107“ на Рамчандър Варадараджан (Sun Microsystems, септември 2000 г.) и „Memory Matters“ на Тони Синтес ( JavaWorld, декември 2001 г.), детайлизират тази идея. За съжаление, решението на първата статия се проваля, тъй като внедряването използва грешен Runtimeметод, докато решението на втората статия има свои собствени несъвършенства:

  • Еднократно обаждане до се Runtime.freeMemory()оказва недостатъчно, защото JVM може да реши да увеличи текущия си размер на купчината по всяко време (особено когато изпълнява събирането на боклук). Освен ако общият размер на купчината вече не е на -Xmx максимален размер, трябва да използваме Runtime.totalMemory()-Runtime.freeMemory()като използван размер на купчината.
  • Изпълнението на едно Runtime.gc()повикване може да не се окаже достатъчно агресивно за искане на сметосъбиране. Можем например да поискаме да стартират и финализатори на обекти. И тъй като Runtime.gc()не е документирано да блокира, докато събирането завърши, е добра идея да изчакате, докато възприеманият размер на купчината се стабилизира.
  • Ако профилираният клас създава някакви статични данни като част от инициализацията на клас за клас (включително инициализатори на статичен клас и поле), паметта на купчината, използвана за първия екземпляр на клас, може да включва тези данни. Трябва да игнорираме пространството на купчината, консумирано от първия екземпляр на класа.

Имайки предвид тези проблеми, представям Sizeofинструмент, с който преглеждам различни класове Java и приложения:

публичен клас Sizeof {public static void main (String [] args) хвърля изключение {// Загрейте всички класове / методи, които ще използваме runGC (); usedMemory (); // Масив за запазване на силни препратки към разпределени обекти final int count = 100000; Обект [] обекти = нов Обект [брой]; дълга купчина1 = 0; // Разпределяме броя + 1 обекта, отхвърляме първия за (int i = -1; i = 0) обекти [i] = обект; else {обект = нула; // Изхвърлете обекта за загряване runGC (); heap1 = usedMemory (); // Направете снимка преди купчина}} runGC (); дълга купчина2 = usedMemory (); // Направете моментна снимка след купчина: окончателен размер int = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'преди' купчина:" + купчина1 + ", 'след купчина:" + купчина2); System.out.println ("делта на купчината:" + (heap2 - heap1) + ", {" + обекти [0].getClass () + "} размер =" + размер + "байта"); за (int i = 0; i <count; ++ i) обекти [i] = null; обекти = нула; } private static void runGC () хвърля изключение {// Помага да се извика Runtime.gc () // като се използват няколко извиквания на методи: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () хвърля изключение {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; за (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаi <брой; ++ i) обекти [i] = null; обекти = нула; } private static void runGC () хвърля изключение {// Помага да се извика Runtime.gc () // като се използват няколко извиквания на методи: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () хвърля изключение {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; за (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаi <брой; ++ i) обекти [i] = null; обекти = нула; } private static void runGC () хвърля изключение {// Помага да се извика Runtime.gc () // като се използват няколко извиквания на методи: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () хвърля изключение {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; за (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаgc () // използвайки няколко извиквания на методи: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () хвърля изключение {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; за (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаgc () // използвайки няколко извиквания на методи: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () хвърля изключение {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; за (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класаThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} частна статична дълго използвана памет () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } частно статично окончателно време на изпълнение s_runtime = Runtime.getRuntime (); } // Край на класа

Sizeofключовите методи са runGC()и usedMemory(). Използвам runGC()метод на обвивка, за да извикам _runGC()няколко пъти, защото изглежда, че прави метода по-агресивен. (Не съм сигурен защо, но е възможно създаването и унищожаването на рамка за извикване на метод предизвиква промяна в кореновия набор за достъпност и подсказва събирача на боклук да работи по-усилено. Освен това, консумирането на голяма част от купчината пространство за създаване на достатъчно работа помага и събирачът на боклук. Като цяло е трудно да се гарантира, че всичко се събира. Точните подробности зависят от JVM и алгоритъма за събиране на боклука.)

Обърнете внимание внимателно на местата, където се позовавам runGC(). Можете да редактирате кода между heap1и heap2декларациите, за да създадете инстанция за всичко, което представлява интерес.

Също така обърнете внимание как се Sizeofотпечатва размерът на обекта: преходното затваряне на данни, изисквано от всички countекземпляри на класа, разделено на count. За повечето класове резултатът ще бъде изразходван от памет от един екземпляр на клас, включително всички негови притежавани полета. Тази стойност на отпечатъка на паметта се различава от данните, предоставени от много търговски профили, които отчитат плитки отпечатъци на паметта (например, ако обектът има int[]поле, неговата консумация на памет ще се покаже отделно).

Резултатите

Нека приложим този прост инструмент към няколко класа, след което да видим дали резултатите отговарят на нашите очаквания.

Забележка: Следните резултати се базират на JDK на Sun на JDK 1.3.1 за Windows. Поради това, което е и не е гарантирано от езика Java и спецификациите на JVM, не можете да приложите тези специфични резултати към други платформи или други внедрения на Java.

java.lang.Object

Е, коренът на всички обекти просто трябваше да бъде първият ми случай. За java.lang.Object, получавам:

„преди“ купчина: 510696, „след“ купчина: 1310696 купчина делта: 800000, {клас java.lang.Object} размер = 8 байта 

И така, равнината Objectотнема 8 байта; Разбира се, никой не бива да се очаква размерът да е 0, като всеки случай, трябва да се носят наоколо полета, че подкрепата на базови операции обичат equals(), hashCode(), wait()/notify(), и така нататък.

java.lang.Integer

Моите колеги и аз често обгръщаме родни intsв Integerекземпляри, за да можем да ги съхраняваме в колекции на Java. Колко ни струва в паметта?

"преди" купчина: 510696, "след" купчина: 2110696 купчина делта: 1600000, {клас java.lang.Integer} размер = 16 байта 

Резултатът от 16 байта е малко по-лош, отколкото очаквах, защото intстойността може да се побере само в 4 допълнителни байта. Използването Integerми струва 300 процента режийни памет в сравнение с това, когато мога да съхранявам стойността като примитивен тип.

java.lang.Дълго

Longтрябва да отнеме повече памет Integer, но не:

"преди" купчина: 510696, "след" купчина: 2110696 купчина делта: 1600000, {клас java.lang.Long} размер = 16 байта 

Ясно е, че действителният размер на обекта в купчината е обект на подравняване на паметта на ниско ниво, извършено от конкретна JVM реализация за определен тип процесор. Изглежда a Longе 8 байта Objectрежийни, плюс 8 байта повече за действителната дълга стойност. За разлика от това, Integerимаше неизползвана 4-байтова дупка, най-вероятно защото JVM, който използвам, принуждава подравняването на обекта на 8-байтова граница на думата.

Масиви

Играта с масиви от примитивен тип се оказва поучителна, отчасти за откриване на някакви скрити режийни и отчасти за оправдаване на друг популярен трик: обвиване на примитивни стойности в масив с размер 1, за да се използват като обекти. Чрез модифициране, Sizeof.main()за да има цикъл, който увеличава създадената дължина на масива при всяка итерация, получавам за intмасиви:

дължина: 0, {клас [I} размер = 16 байта дължина: 1, {клас [I} размер = 16 байта дължина: 2, {клас [I} размер = 24 байта дължина: 3, {клас [I} размер = 24 байта дължина: 4, {клас [I} размер = 32 байта дължина: 5, {клас [I} размер = 32 байта дължина: 6, {клас [I} размер = 40 байта дължина: 7, {клас [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 

и за charмасиви:

дължина: 0, {клас [C} размер = 16 байта дължина: 1, {клас [C} размер = 16 байта дължина: 2, {клас [C} размер = 16 байта дължина: 3, {клас [C} размер = Дължина 24 байта: 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 

По-горе отново се появяват доказателства за 8-байтово подравняване. Освен това, в допълнение към неизбежните Object8-байтови режийни, примитивен масив добавя още 8 байта (от които поне 4 байта поддържат lengthполето). И използването int[1]изглежда не предлага никакви предимства на паметта над Integerекземпляр, освен може би като променяща се версия на същите данни.

Многомерни масиви

Multidimensional arrays offer another surprise. Developers commonly employ constructs like int[dim1][dim2] in numerical and scientific computing. In an int[dim1][dim2] array instance, every nested int[dim2] array is an Object in its own right. Each adds the usual 16-byte array overhead. When I don't need a triangular or ragged array, that represents pure overhead. The impact grows when array dimensions greatly differ. For example, a int[128][2] instance takes 3,600 bytes. Compared to the 1,040 bytes an int[256] instance uses (which has the same capacity), 3,600 bytes represent a 246 percent overhead. In the extreme case of byte[256][1], the overhead factor is almost 19! Compare that to the C/C++ situation in which the same syntax does not add any storage overhead.

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!

В действителност Strings могат да консумират дори повече памет, отколкото предполагат техните дължини: Strings, генерирани от StringBuffers (или изрично, или чрез оператора за конкатенация '+') вероятно имат charмасиви с дължини, по-големи от отчетените Stringдължини, тъй като StringBuffers обикновено започват с капацитет 16 , след това го удвоете при append()операции. Така например, createString(1) + ' 'завършва с charмасив с размер 16, а не 2.

И какво ще правим?

„Всичко това е много добре, но нямаме друг избор, освен да използваме Strings и други типове, предоставени от Java, нали?“ Чувам, че питате. Нека разберем.

Класове на обвивки