Sizeof dla Java

26 grudnia 2003

P: Czy Java ma operator taki jak sizeof () w C?

A: Powierzchowne odpowiedzią jest, że Java nie przewiduje czegoś takiego C użytkownika sizeof(). Zastanówmy się jednak, dlaczego programista Java może czasami tego chcieć.

Programista AC sam zarządza większością alokacji pamięci struktury danych i sizeof()jest niezbędny do znajomości rozmiarów bloków pamięci do przydzielenia. Dodatkowo alokatory pamięci w C malloc()nie robią prawie nic, jeśli chodzi o inicjalizację obiektu: programista musi ustawić wszystkie pola obiektów, które są wskaźnikami do dalszych obiektów. Ale kiedy wszystko jest powiedziane i zakodowane, alokacja pamięci C / C ++ jest dość wydajna.

Dla porównania, alokacja i konstrukcja obiektu Java są ze sobą powiązane (nie można użyć przydzielonej, ale niezainicjowanej instancji obiektu). Jeśli klasa Java definiuje pola, które są odwołaniami do dalszych obiektów, często ustawia się je również podczas konstruowania. Dlatego też alokacja obiektu Java często przydziela wiele połączonych ze sobą instancji obiektów: graf obiektów. W połączeniu z automatycznym usuwaniem elementów bezużytecznych jest to zbyt wygodne i może sprawić, że poczujesz się, jakbyś nigdy nie musiał martwić się o szczegóły alokacji pamięci Java.

Oczywiście działa to tylko w przypadku prostych aplikacji Java. W porównaniu z C / C ++ równoważne struktury danych Java zwykle zajmują więcej pamięci fizycznej. W przypadku tworzenia oprogramowania dla przedsiębiorstw zbliżenie się do maksymalnej dostępnej pamięci wirtualnej w dzisiejszych 32-bitowych maszynach JVM jest częstym ograniczeniem dotyczącym skalowalności. W związku z tym programista Java mógłby skorzystać z sizeof()czegoś podobnego, aby mieć oko na to, czy jego struktury danych stają się zbyt duże lub czy zawierają wąskie gardła pamięci. Na szczęście odbicie w Javie pozwala dość łatwo napisać takie narzędzie.

Zanim przejdę dalej, zrezygnuję z częstych, ale błędnych odpowiedzi na pytanie z tego artykułu.

Błąd: Sizeof () nie jest potrzebny, ponieważ rozmiary podstawowych typów Java są stałe

Tak, Java intma 32 bity we wszystkich maszynach JVM i na wszystkich platformach, ale jest to tylko wymóg specyfikacji języka dla dostrzegalnej przez programistę szerokości tego typu danych. Jest intto zasadniczo abstrakcyjny typ danych i można go zarchiwizować, powiedzmy, 64-bitowym słowem pamięci fizycznej na komputerze 64-bitowym. To samo dotyczy typów niepierwotnych: specyfikacja języka Java nie mówi nic o tym, jak pola klas powinny być wyrównane w pamięci fizycznej, ani o tym, że tablica wartości logicznych nie może zostać zaimplementowana jako kompaktowy wektor bitowy w JVM.

Błąd: Możesz zmierzyć rozmiar obiektu, serializując go do strumienia bajtów i patrząc na wynikową długość strumienia

Przyczyną tego nie jest to, że układ serializacji jest tylko zdalnym odzwierciedleniem prawdziwego układu w pamięci. Jednym z łatwych sposobów, aby to zobaczyć, jest przyjrzenie się, jak Stringsą serializowane: w pamięci każdy charma co najmniej 2 bajty, ale w postaci serializowanej Strings są zakodowane w UTF-8, więc każda zawartość ASCII zajmuje połowę mniej miejsca.

Inne podejście do pracy

Możesz sobie przypomnieć „Java Tip 130: Czy znasz rozmiar danych?” w którym opisano technikę opartą na tworzeniu dużej liczby identycznych instancji klas i dokładnym mierzeniu wynikającego z tego wzrostu wielkości stosu używanej maszyny JVM. W stosownych przypadkach ten pomysł działa bardzo dobrze i faktycznie wykorzystam go do załadowania alternatywnego podejścia w tym artykule.

Zwróć uwagę, że Sizeofklasa Java Tip 130 wymaga nieaktywnej maszyny JVM (tak, aby działanie sterty było spowodowane tylko alokacjami obiektów i czyszczeniem elementów bezużytecznych żądanych przez wątek pomiarowy) i wymaga dużej liczby identycznych instancji obiektów. Nie działa to, gdy chcesz zmienić rozmiar pojedynczego dużego obiektu (być może jako część danych wyjściowych śledzenia debugowania), a zwłaszcza gdy chcesz sprawdzić, co sprawiło, że był on tak duży.

Jaki jest rozmiar obiektu?

Powyższa dyskusja zwraca uwagę na kwestię filozoficzną: biorąc pod uwagę, że zwykle masz do czynienia z grafami obiektów, jaka jest definicja rozmiaru obiektu? Czy jest to tylko rozmiar badanej instancji obiektu, czy rozmiar całego wykresu danych zakorzenionego w instancji obiektu? To ostatnie ma zwykle większe znaczenie w praktyce. Jak zobaczysz, sprawy nie zawsze są tak jasne, ale na początek możesz zastosować następujące podejście:

  • Wielkość instancji obiektu może być (w przybliżeniu) określona przez zsumowanie wszystkich jej niestatycznych pól danych (w tym pól zdefiniowanych w nadklasach)
  • W przeciwieństwie do, powiedzmy, C ++, metody klas i ich wirtualność nie mają wpływu na rozmiar obiektu
  • Superinterfejsy klas nie mają wpływu na rozmiar obiektu (patrz uwaga na końcu tej listy)
  • Pełny rozmiar obiektu można uzyskać jako zamknięcie całego grafu obiektu zakorzenionego w obiekcie początkowym
Uwaga: zaimplementowanie dowolnego interfejsu Java oznacza jedynie zaznaczenie danej klasy i nie dodaje żadnych danych do jej definicji. W rzeczywistości JVM nawet nie sprawdza, czy implementacja interfejsu zapewnia wszystkie metody wymagane przez interfejs: jest to ściśle obowiązkiem kompilatora w bieżących specyfikacjach.

Aby załadować proces, dla pierwotnych typów danych używam fizycznych rozmiarów mierzonych przez Sizeofklasę Java Tip 130 . Jak się okazuje, w przypadku typowych 32-bitowych maszyn JVM zwykły rozmiar java.lang.Objectzajmuje 8 bajtów, a podstawowe typy danych mają zwykle najmniejszy rozmiar fizyczny, który może pomieścić wymagania językowe (z wyjątkiem booleancałego bajtu):

// rozmiar powłoki java.lang.Object w bajtach: public static final int OBJECT_SHELL_SIZE = 8; public static final int OBJREF_SIZE = 4; public static final int LONG_FIELD_SIZE = 8; public static final int INT_FIELD_SIZE = 4; public static final int SHORT_FIELD_SIZE = 2; public static final int CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; public static final int BOOLEAN_FIELD_SIZE = 1; public static final int DOUBLE_FIELD_SIZE = 8; public static final int FLOAT_FIELD_SIZE = 4;

(Ważne jest, aby zdać sobie sprawę, że te stałe nie są zakodowane na stałe i muszą być mierzone niezależnie dla danej maszyny JVM). Oczywiście naiwne sumowanie rozmiarów pól obiektów pomija problemy z wyrównaniem pamięci w JVM. Wyrównanie pamięci ma znaczenie (jak pokazano na przykład dla prymitywnych typów tablic w Java Tip 130), ale myślę, że pogoń za tak niskopoziomowymi szczegółami jest nieopłacalna. Takie szczegóły są nie tylko zależne od dostawcy JVM, ale także nie są pod kontrolą programisty. Naszym celem jest dokładne przypuszczenie rozmiaru obiektu i miejmy nadzieję, że pole klasy może być zbędne; lub kiedy pole powinno być leniwie zaludnione; lub gdy potrzebna jest bardziej zwarta zagnieżdżona struktura danych itp. Aby uzyskać absolutną fizyczną precyzję, zawsze możesz wrócić do Sizeofklasy w Java Tip 130.

Aby pomóc w profilowaniu tego, co składa się na instancję obiektu, nasze narzędzie nie tylko obliczy rozmiar, ale także zbuduje pomocną strukturę danych jako produkt uboczny: wykres składający się z IObjectProfileNodes:

interfejs IObjectProfileNode {Object object (); Nazwa ciągu (); int size (); int refcount (); IObjectProfileNode rodzic (); IObjectProfileNode [] dzieci (); IObjectProfileNode shell (); IObjectProfileNode [] path (); IObjectProfileNode root (); int pathlength (); wartość logiczna (filtr INodeFilter, odwiedzający INodeVisitor); Zrzut ciągu (); } // Koniec interfejsu

IObjectProfileNodes są ze sobą połączone niemal dokładnie w taki sam sposób, jak oryginalny graf obiektowy, IObjectProfileNode.object()zwracając rzeczywisty obiekt reprezentowany przez każdy węzeł. IObjectProfileNode.size()zwraca całkowity rozmiar (w bajtach) poddrzewa obiektu zakorzenionego w instancji obiektu tego węzła. Jeśli instancja obiektu łączy się z innymi obiektami za pośrednictwem niezerowych pól instancji lub odwołań zawartych w polach tablicowych, to IObjectProfileNode.children()będzie odpowiadała lista potomnych węzłów grafów, posortowana w kolejności malejącej. I odwrotnie, dla każdego węzła innego niż początkowy IObjectProfileNode.parent()zwraca jego rodzica. W ten sposób cała kolekcja IObjectProfileNodes tnie i kroi oryginalny obiekt i pokazuje, jak partycjonowane są w nim dane. Ponadto nazwy węzłów wykresu pochodzą z pól klas i badają ścieżkę węzła na wykresie (IObjectProfileNode.path()) umożliwia prześledzenie łączy własności od oryginalnej instancji obiektu do dowolnego wewnętrznego fragmentu danych.

Być może zauważyłeś podczas lektury poprzedniego akapitu, że dotychczasowy pomysł nadal jest niejasny. Jeśli podczas przechodzenia przez graf obiektu napotkasz tę samą instancję obiektu więcej niż raz (tj. Wskazuje na nią więcej niż jedno pole w dowolnym miejscu na wykresie), w jaki sposób przypisujesz jej własność (wskaźnik nadrzędny)? Rozważ ten fragment kodu:

 Object obj = new String [] {new String ("JavaWorld"), new String ("JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

Myślenie o przechodzeniu przez graf i najkrótszych ścieżkach powinno w tym miejscu zadzwonić: przeszukiwanie wszerz to algorytm przechodzenia grafu, który gwarantuje znalezienie najkrótszej ścieżki od węzła początkowego do dowolnego innego osiągalnego węzła grafu.

Po tych wszystkich wstępach, oto podręcznikowa implementacja przechodzenia takiego wykresu. (Pominięto niektóre szczegóły i metody pomocnicze; szczegółowe informacje można znaleźć w pliku do pobrania tego artykułu):