Jeśli ktoś podobnie jak ja zastanawia się “ciekawe ile ten obiekt zajmuje miejsca w pamięci?” to mam dla was dobrą wiadomość! Java Object Layout (JOL) jest mini-programem, który potrafi policzyć jaki będzie rozmiar obiektu przechowywanego w pamięci. Oczywiście dobrze wiemy, iż w aktualnych czasach pamięć jest “tania”, jednakże jeśli chcielibyśmy się przekonać, który obiekt zajmuje mniej miejsca w pamięci to zapraszam do artykułu.
SerialNumber
Zaczniemy od stworzenia klasy reprezentującej numer seryjny. Na początku posiada ona tylko jedno pole typu int
:
class SerialNumber { private final int value; SerialNumber(final int value) { this.value = value; } }
Typy prymitywne
Autorzy Javy założyli, iż ich platforma będzie przenaszalna. Aby to osiągnąć musieli rozwiązać wiele problemów. Jednym z nich były różne rozmiary typów. Dlatego też wirtualna maszyna Javy posiada z góry określone rozmiary dla typów prymitywnych:
byte
– 1 bajtshort
– 2 bajtyint
– 4 bajtylong
– 8 bajtówfloat
– 4 bajtydouble
– 8 bajtówchar
– 2 bajtyboolean
– rozmiar zależy od implementacji JVM (najczęściej jest to 1 bajt)
Więc jak dotychczas nasz obiekt SerialNumber
powinien zajmować 4 bajty.
Typy obiektowe
Typy prymitywne w Javie muszą znajdować się wewnątrz obiektu. Każdy obiekt zbudowany jest z typów prymitywnych lub z kolejnych typów obiektowych (które, “na samym dole” składają się z typów prymitywnych). Każdy typ obiektowy oprócz wartości pól przechowuje informacje o samym sobie (metadane). Metadane te składają się z:
class
– wskaźnik do typu klasy w naszym przypadku – 4 bajtyflagi
– przechowują informacje o stanie obiektu. Między innymi shape obiektu, czyli czy jest to klasa czy tablica oraz hash code – 4 bajtylock
– monitor, który będzie wykorzystywany do mutex’ów – 4 bajtysize
– rozmiar tablicy (wartość ta obecna jest tylko dla typów tablicowych) – 4 bajty
Posiadając informację na temat typów obiektowych i prymitywnych wiemy już, iż nasz obiekt powinien zajmować 12 + 4 = 16 bajtów. Aby to sprawdzić wykorzystamy do tego narzędzie JOL (skrót od Java Object Layout).
Java Object Layout
Jak pisałem we wstępie JOL jest narzędziem do sprawdzania rozmiaru obiektów. Zacznijmy od dodania zależności do projektu:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
Na samym początku wypiszmy informację o naszej aktualnej wirtualnej maszynie:
System.out.println(VM.current().details());
W odpowiedzi dostaniemy wiele ciekawych informacji:
# Running 64-bit HotSpot VM. # Using compressed oop with 0-bit shift. # Using compressed klass with 0-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
- Uruchamiamy nasz program na 64 bitowej wersji JVM – HotSpot
- Compressed Oops i Klass opiszemy w przyszłości
- Obiekty będą wyrównane do 8 bajtów (to jest bardzo ważna informacja, dalej w artykule się to wyjaśni)
- Rozmiary typów w naszej JVM (pierwsza wartość 4 bajty to rozmiar referencji)
Wypiszmy więc rozmiar naszego obiektu SerialNumber
:
final SerialNumber serialNumber = new SerialNumber(123456); System.out.println(ClassLayout.parseClass(SerialNumber.class).toPrintable(serialNumber));
W odpowiedzi otrzymamy:
pl.codecouple.SerialNumber object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) 00 14 fd 16 12 4 int SerialNumber.value 123456 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 0 – 4 bajtów to flagi naszej klasy
- 4 – 8 bajtów to miejsce na monitor
- 8 – 12 bajtów to wskaźnik na typ obiektu, w tym przypadku jest to
pl.codecouple.SerialNumber
- 12 – 16 bajtów nasza wartość
int
Czyli zgodnie z założeniami nasz obiekt zajmuje 16 bajtów pamięci.
Wyrównanie do 8 bajtów
Spróbujmy do naszej klasy dodać kolejne pole typu int
. W teorii nasza klasa powinna zajmować 20 bajtów:
class SerialNumber { private final int value; private final int secondValue; SerialNumber(final int value, int secondValue) { this.value = value; this.secondValue = secondValue; } }
Sprawdzamy rozmiar:
pl.codecouple.SerialNumber object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) 00 14 c2 16 12 4 int SerialNumber.value 123456 16 4 int SerialNumber.secondValue 1234567 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Nasza klasa powinna zajmować 20 bajtów, natomiast zajmuje ich aż 24. Jest to tak zwane wyrównanie do 8 bajtów. Java “pod spodem” wyrównuje obiekty do wielokrotności 8 bajtów. Czyli w naszym przypadku najbliższą wielokrotnością 8 przy 20 jest liczba 24 (dopełnianie to wynika z działania aktualnego JVM’a, “Objects are 8 bytes aligned“). Informacja o tym dopełnieniu znajduje się również na powyższym zrzucie rozmiaru.
Rozmiar referencji
Co w przypadku jak oprócz typów prymitywnych w naszej klasie pojawią się typy obiektowe? Rozmiar referencji do obiektu zajmuje 4 lub 8 bajtów w zależności czy uruchamiamy program na 32 lub 64 bitowej wirtualnej maszynie Javy. Sprawdźmy rozmiar obiektu korzystając z JOL:
class SerialNumber { private final int value; private final int secondValue; private final IntNumber objectValue; SerialNumber(final int value, int secondValue, IntNumber objectValue) { this.value = value; this.secondValue = secondValue; this.objectValue = objectValue; } } class IntNumber { private final int value; IntNumber(int value) { this.value = value; } }
Wynik:
pl.codecouple.SerialNumber object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) 00 14 c7 16 12 4 int SerialNumber.value 123456 16 4 int SerialNumber.secondValue 1234567 20 4 IntNumber SerialNumber.objectValue (object) Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Gdy pierwszy raz pracowałem z JOL to wynik ten wydał mi się zbyt mały. Ponieważ, założyłem iż mój obiekt wewnętrzny będzie zajmował 16 bajtów (12 nagłówki + 4 dla int
). Natomiast klasa ClassLayout
zwraca rozmiar tylko aktualnej klasy wraz z referencją do obiektów zagnieżdzonych bez zliczania ich wewnętrznej reprezentacji. W tym przypadku wynik jest poprawny, ponieważ referencja zajmuje 4 bajty.
Graph Layout
Aby policzyć całkowity rozmiar obiektu wraz z wszystkim typami referencyjnymi użytymi w środku powinniśmy skorzystać z klasy GraphLayout
:
final SerialNumber serialNumber = new SerialNumber(123456, 1234567, new IntNumber(1)); System.out.println(GraphLayout.parseInstance(serialNumber).toFootprint());
Tym razem wynik się zgadza:
pl.codecouple.SerialNumber@66cd51c3d footprint: COUNT AVG SUM DESCRIPTION 1 16 16 pl.codecouple.IntNumber 1 24 24 pl.codecouple.SerialNumber 2 40 (total)
Github
Całość jak zawsze na GitHub’ie.