Co to jest JOL?
JOL (Java Object Layout) to mały zestaw narzędzi do analizy obiektów w JVM. Narzędzia te wykorzystuje Unsafe, JVMTI i Serviceability Agent (SA) do dekodowania rzeczywistego układu obiektów i referencji. Dzięki temu JOL jest znacznie dokładniejszy niż inne narzędzia bazujące na zrzutach sterty, założeniach specyfikacji itp.
Jak się do niego dobrać?
Dodajemy sobie zależność do maven’a:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
Aby dobrać się do wirtualnej maszyny mamy do dyspozycji taką klasę:
org.openjdk.jol.vm.VM
Która ma jedną statyczną metodę publiczną:
VirtualMachine.current()
Zwraca obiekt typu:
org.openjdk.jol.vm.VirtualMachine
Dzięki tej klasie mamy możliwość dobrać się do danych JVM np:
Odpalenie czegoś takiego:
VM.current().details();
Daje nam informacje o maszynie wirtualnej, np:
# Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # WARNING | Compressed references base/shifts are guessed by the experiment! # WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE. # WARNING | Make sure to attach Serviceability Agent to get the reliable addresses. # 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]
Jak chcemy sprawdzić wielość nagłówka obiektu w pamięci wystarczy wpisać:
VM.current().objectHeaderSize()
Sprawdzenie czy dana klasa jest adnotowana przez adnotacje @Contended:
ContendedSupport.isContended(StandardObject.class)
W pakiecie JOL mamy również do dyspozycji kilka klas utils:
org.openjdk.jol.util.ClassUtils
umożliwiający dynamiczne ładowanie klas, również w systemowych ClassLoaderze
org.openjdk.jol.util.IOUtils
util pozwalający bezpiecznie zamykać strumienie
org.openjdk.jol.util.MathUtil
kilka matematycznych operacji
org.openjdk.jol.util.Multiset
Klasa opakowująca java.util.HashMap służąca jako inkrementer
org.openjdk.jol.util.ObjectUtils
w klasie są dwie metody, value i safeToString. Metoda value wyciąga wartość dla wybranego pola z klasy na trzy różne sposoby, metoda safeToString wykonuje toString() na typach array i primitive
Za pomocą dostępu do VM możemy wyciągać dużo szybciej dane z obiektów za pomocą „refleksji” – a tak na prawdę bezpośrednio odwołując się do pamięci.
Przykładowy benchmark:
Setup dla benchmarka:
private StandardObject object; private Field field; private VirtualMachine vm; private long offset; @Setup public void setup () { object = new StandardObject(); object.setVal(100.0); vm = VM.current(); try { field = StandardObject.class.getDeclaredField("val"); } catch (NoSuchFieldException e) { e.printStackTrace(); } offset = vm.fieldOffset(field); }
Standardowe wyciąganie danych przez refleksje:
@Benchmark public void reflect () { try { field.setAccessible(true); blackhole.consume(field.get(object)); } catch (Exception e) { e.printStackTrace(); } }
Wyciąganie danych bezpośrednio z pamięci
@Benchmark public void reflectVM () { try { blackhole.consume(vm.getDouble(object, offset)); } catch (Exception e) { e.printStackTrace(); } }
Wynik benchmarku:
Benchmark Mode Cnt Score Error Units BenchmarkApp.reflect thrpt 5 159966.722 ± 4850.180 ops/ms BenchmarkApp.reflectVM thrpt 5 259729.201 ± 6932.689 ops/ms
Wynik pokazuje, że możemy spokojnie zastosować JOL do wyciągania danych za pomocą refleksji. Oczywiście może to być istotne w sytuacji gdy wykonuje po kilka set a nawet tysięcy takich operacji na sekundę.
Analiza obiektów za pomocą JOL:
Sprawdzanie adresu obiektu:
VM.current().addressOf(obiekt);
Sprawdzanie wielkości obiektu, w przypadku obiektów złożonych nagłówka:
VM.current().sizeOf(obiekt);
Wyświetlanie danych o obiekcie:
GraphLayout.parseInstance(obiekt).toPrintable();
Wynik:
net.szwadron.StandardObject@47d384eed object externals: ADDRESS SIZE TYPE PATH VALUE 782a82d58 24 java.lang.String .string (object) 782a82d70 96 [C .string.value [f, s, d, f, d, s, f, d, s, f, d, s, s, d, d, s, d, s, c, d, v, d, s, v, 2, 3, r, 2, 3, e, 2, 3, e, d, w, d, 2, 3] 782a82dd0 24 java.lang.String .standardObject.string (object) 782a82de8 112 [C .standardObject.string.value [f, s, d, f, d, s, f, d, s, f, d, s, s, d, d, s, d, s, c, d, v, d, s, v, 2, 3, r, 2, 3, e, 2, 3, e, d, w, d, 2, 3, e, w, r, e, w, r, w, e] 782a82e58 24 (something else) (somewhere else) (something else) 782a82e70 24 net.szwadron.StandardObject (object) 782a82e88 384 (something else) (somewhere else) (something else) 782a83008 24 java.lang.Double .val 10.0 782a83020 24 net.szwadron.StandardObject .standardObject (object)
Wyświeltenie tylko i wyłacznie wilkości:
GraphLayout.parseInstance(obiekt).toFootprint();
Wynik:
net.szwadron.StandardObject@47d384eed footprint: COUNT AVG SUM DESCRIPTION 2 104 208 [C 1 24 24 java.lang.Double 2 24 48 java.lang.String 2 24 48 net.szwadron.StandardObject 7 328 (total)
Sprawdzenie całkowitej ilości obiektów:
GraphLayout.parseInstance(obiekt).totalCount();
No i na koniec sprawdzenie tylko wielkości obiektów:
GraphLayout.parseInstance(obiekt).totalSize();
Zwraca nam sumę wszystkich obiektów. Oczywiście wszystkie te dane mamy również wyżej, ale jeśli chcemy mieć jakieś metryki z wielkości danych jakie przetwarzamy lub wiedzieć ile pamięci potrzebujemy to ostatnia metoda w pełni nam to ogarnie.
Na koniec tylko wspomnę, że w JOL również mamy coś takiego jak HeapDumpReader który pozwala nam załadować do pamięci dumpa pamięci JVM i go analizować, czyli jakby ktoś chciał sobie napisać własnego analizera do pamięci.
Źródło:
http://openjdk.java.net/projects/code-tools/jol/