Java Tip 130: Czy znasz rozmiar swoich danych?

Ostatnio pomogłem zaprojektować aplikację serwerową Java, która przypominała bazę danych w pamięci. Oznacza to, że zdecydowaliśmy się na buforowanie ton danych w pamięci, aby zapewnić superszybką wydajność zapytań.

Gdy uruchomiliśmy prototyp, naturalnie zdecydowaliśmy się profilować ślad pamięci danych po jego przeanalizowaniu i załadowaniu z dysku. Niezadowalające początkowe wyniki skłoniły mnie jednak do poszukiwania wyjaśnień.

Uwaga: możesz pobrać kod źródłowy tego artykułu z zasobów.

Narzędzie

Ponieważ Java celowo ukrywa wiele aspektów zarządzania pamięcią, odkrycie, ile pamięci zajmują obiekty, wymaga trochę pracy. Możesz użyć tej Runtime.freeMemory()metody do pomiaru różnic wielkości sterty przed i po przydzieleniu kilku obiektów. Kilka artykułów, takich jak „Pytanie tygodnia nr 107” Ramchandera Varadarajana (Sun Microsystems, wrzesień 2000) i „Memory Matters” Tony'ego Sintesa ( JavaWorld, grudzień 2001), szczegółowo opisują ten pomysł. Niestety, rozwiązanie z pierwszego artykułu zawodzi, ponieważ implementacja wykorzystuje złą Runtimemetodę, podczas gdy rozwiązanie z drugiego artykułu ma swoje własne niedoskonałości:

  • Pojedyncze wywołanie Runtime.freeMemory()okazuje się niewystarczające, ponieważ maszyna JVM może zdecydować o zwiększeniu swojego aktualnego rozmiaru sterty w dowolnym momencie (zwłaszcza gdy uruchamia czyszczenie pamięci). O ile całkowity rozmiar sterty nie osiągnął już maksymalnego rozmiaru -Xmx, powinniśmy użyć Runtime.totalMemory()-Runtime.freeMemory()jako używanego rozmiaru sterty.
  • Wykonywanie pojedynczego Runtime.gc()wywołania może nie okazać się wystarczająco agresywne, aby zażądać czyszczenia pamięci. Moglibyśmy na przykład zażądać uruchomienia finalizatorów obiektów. A ponieważ Runtime.gc()nie udokumentowano, że blokuje się do zakończenia zbierania danych, dobrze jest poczekać, aż postrzegany rozmiar sterty ustabilizuje się.
  • Jeśli klasa profilowana tworzy dowolne dane statyczne w ramach inicjalizacji klasy dla klasy (w tym inicjatory klas statycznych i pól), pamięć sterty używana dla wystąpienia pierwszej klasy może zawierać te dane. Powinniśmy zignorować miejsce na stosie zajmowane przez instancję pierwszej klasy.

Biorąc pod uwagę te problemy, przedstawiam Sizeofnarzędzie, za pomocą którego podsłuchuję różne klasy jądra i aplikacji Javy:

public class Sizeof {public static void main (String [] args) throws Exception {// Rozgrzej wszystkie klasy / metody, których użyjemy runGC (); używana pamięć (); // Tablica, aby zachować silne odniesienia do przydzielonych obiektów final int count = 100000; Object [] objects = new Object [count]; long heap1 = 0; // Przydziel liczbę + 1 obiektów, odrzuć pierwszy z nich dla (int i = -1; i = 0) objects [i] = object; else {object = null; // Odrzuć rozgrzewający obiekt runGC (); heap1 = usedMemory (); // Wykonaj migawkę przed stertą}} runGC (); long heap2 = usedMemory (); // Wykonaj migawkę po sterty: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'przed' stertą:" + sterta1 + ", 'po' sterty:" + sterta2); System.out.println ("delta sterty:" + (sterta2 - sterta1) + ", {" + obiekty [0].getClass () + "} size =" + size + "bajty"); for (int i = 0; i <count; ++ i) objects [i] = null; obiekty = null; } private static void runGC () rzuca wyjątek {// Wywołanie Runtime.gc () // przy użyciu kilku wywołań metod: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () zgłasza wyjątek {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 (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęći <count; ++ i) obiekty [i] = null; obiekty = null; } private static void runGC () rzuca wyjątek {// Wywołanie Runtime.gc () // przy użyciu kilku wywołań metod: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () zgłasza wyjątek {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 (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęći <count; ++ i) obiekty [i] = null; obiekty = null; } private static void runGC () rzuca wyjątek {// Wywołanie Runtime.gc () // przy użyciu kilku wywołań metod: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () zgłasza wyjątek {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 (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęćgc () // używając kilku wywołań metod: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () zgłasza wyjątek {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 (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęćgc () // używając kilku wywołań metod: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () zgłasza wyjątek {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 (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęćThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęćThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} prywatny statyczny long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Koniec zajęć

SizeofKluczowymi metodami są runGC()i usedMemory(). Używam runGC()metody opakowującej, aby wywołać _runGC()kilka razy, ponieważ wydaje się, że metoda jest bardziej agresywna. (Nie jestem pewien dlaczego, ale możliwe jest, że utworzenie i zniszczenie ramki stosu wywołań metody powoduje zmianę w zestawie głównym osiągalności i zachęca moduł odśmiecania pamięci do cięższej pracy. Ponadto, zużywanie dużej części miejsca na sterty, aby stworzyć wystarczającą ilość pracy pomaga też moduł odśmiecania pamięci. Ogólnie rzecz biorąc, trudno jest upewnić się, że wszystko zostało zebrane. Dokładne szczegóły zależą od maszyny JVM i algorytmu czyszczenia pamięci).

Zwróć uwagę na miejsca, w których wzywam runGC(). Możesz edytować kod między deklaracjami heap1i, heap2aby utworzyć wystąpienie dowolnego interesującego elementu.

Zwróć także uwagę, jak Sizeofwypisuje rozmiar obiektu: przechodnie zamknięcie danych wymaganych przez wszystkie countinstancje klas, podzielone przez count. W przypadku większości klas wynik będzie zużyty przez pojedynczą instancję klasy, w tym wszystkie należące do niej pola. Ta wartość śladu pamięci różni się od danych dostarczanych przez wiele komercyjnych profilerów, które raportują niewielkie ślady pamięci (na przykład, jeśli obiekt ma int[]pole, jego zużycie pamięci będzie wyświetlane osobno).

Wyniki

Zastosujmy to proste narzędzie do kilku zajęć, a następnie zobaczmy, czy wyniki odpowiadają naszym oczekiwaniom.

Uwaga: Poniższe wyniki są oparte na JDK 1.3.1 firmy Sun dla systemu Windows. Ze względu na to, co jest, a czego nie gwarantują specyfikacje języka Java i maszyny JVM, nie można zastosować tych konkretnych wyników do innych platform ani innych implementacji języka Java.

java.lang.Object

Cóż, korzeń wszystkich obiektów musiał być moim pierwszym przypadkiem. Za java.lang.Objectotrzymuję:

„before” heap: 510696, „after” heap: 1310696 heap delta: 800000, {class java.lang.Object} size = 8 bajtów 

Czyli zwykły Objectzajmuje 8 bajtów; Oczywiście, nikt nie powinien oczekiwać, że wielkość wynosi 0, a każdy przypadek musi nosić ze sobą pola, które operacje bazy wsparcia podoba equals(), hashCode(), wait()/notify(), i tak dalej.

java.lang.Integer

Moi koledzy i ja często pakujemy elementy natywne intsw Integerinstancje, abyśmy mogli przechowywać je w kolekcjach Java. Ile kosztuje nas pamięć?

„before” heap: 510696, „after” heap: 2110696 heap delta: 1600000, {class java.lang.Integer} size = 16 bajtów 

Wynik 16-bajtowy jest trochę gorszy niż się spodziewałem, ponieważ intwartość może zmieścić się w zaledwie 4 dodatkowych bajtach. Korzystanie z wartości Integerkosztuje mnie 300 procent narzutu pamięci w porównaniu z sytuacją, gdy mogę zapisać wartość jako typ pierwotny.

java.lang.Long

Longpowinien zająć więcej pamięci niż Integer, ale nie:

„before” heap: 510696, „after” heap: 2110696 heap delta: 1600000, {class java.lang.Long} size = 16 bajtów 

Oczywiście rzeczywisty rozmiar obiektu na stercie podlega wyrównaniu pamięci niskiego poziomu przez określoną implementację maszyny JVM dla określonego typu procesora. Wygląda na to, że a Longto 8 bajtów Objectnarzutu plus 8 bajtów więcej na rzeczywistą długą wartość. W przeciwieństwie do tego, Integermiał nieużywany 4-bajtowy otwór, najprawdopodobniej dlatego, że używana przeze mnie maszyna JVM wymusza wyrównanie obiektów na granicy 8-bajtowego słowa.

Tablice

Zabawa z tablicami typów prymitywnych okazuje się pouczająca, częściowo po to, aby odkryć jakiekolwiek ukryte narzuty, a częściowo po to, aby usprawiedliwić inną popularną sztuczkę: zawijanie prymitywnych wartości w tablicę o rozmiarze 1 w celu użycia ich jako obiektów. Modyfikując, Sizeof.main()aby mieć pętlę, która zwiększa długość utworzonej tablicy przy każdej iteracji, otrzymuję dla inttablic:

length: 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 = 24 bajty długość: 4, {klasa [I} rozmiar = 32 bajty długość: 5, {klasa [I} rozmiar = 32 bajty długość: 6, {klasa [I} rozmiar = 40 bajtów]: 7, {klasa [I} size = 40 bajtów length: 8, {class [I} size = 48 bytes length: 9, {class [I} size = 48 bajtów length: 10, {class [I} size = 56 bajtów 

a dla chartablic:

length: 0, {class [C} size = 16 bajtów length: 1, {class [C} size = 16 bytes length: 2, {class [C} size = 16 bytes length: 3, {class [C} size = 24 bajty długość: 4, {class [C} size = 24 bajty długość: 5, {class [C} size = 24 bajty długość: 6, {class [C} size = 24 bajty]: 7, {class [C} size = 32 bajty length: 8, {class [C} size = 32 bajty length: 9, {class [C} size = 32 bajty długość: 10, {class [C} size = 32 bajty] 

Powyżej ponownie pojawiają się dowody wyrównania 8-bajtowego. Ponadto, oprócz nieuniknionego Object8-bajtowego narzutu, tablica pierwotna dodaje kolejne 8 bajtów (z czego co najmniej 4 bajty obsługują lengthpole). int[1]Wydaje się, że użycie nie zapewnia żadnej przewagi w zakresie pamięci w porównaniu z Integerinstancją, z wyjątkiem być może modyfikowalnej wersji tych samych danych.

Tablice wielowymiarowe

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!

W rzeczywistości Strings mogą zajmować nawet więcej pamięci, niż sugerują ich długości: Strings wygenerowane z StringBuffers (jawnie lub za pomocą operatora konkatenacji `` + '') prawdopodobnie mają chartablice o długościach większych niż podane Stringdługości, ponieważ StringBuffers zazwyczaj zaczynają się od pojemności 16 , a następnie podwoić ją w przypadku append()operacji. Na przykład createString(1) + ' 'kończy się chartablicą o rozmiarze 16, a nie 2.

Co robimy?

„To wszystko bardzo dobrze, ale nie mamy innego wyboru, jak tylko użyć Strings i innych typów udostępnionych przez Javę, prawda?” Słyszę, jak pytasz. Dowiedzmy Się.

Klasy opakowujące