Wirtualna maszyna Java - kompilator JIT
W tym rozdziale dowiemy się o kompilatorze JIT i różnicach między językami kompilowanymi i interpretowanymi.
Języki kompilowane i interpretowane
Języki takie jak C, C ++ i FORTRAN są językami kompilowanymi. Ich kod jest dostarczany jako kod binarny przeznaczony dla maszyny bazowej. Oznacza to, że kod wysokiego poziomu jest kompilowany od razu do kodu binarnego przez statyczny kompilator napisany specjalnie dla podstawowej architektury. Utworzony plik binarny nie będzie działał na żadnej innej architekturze.
Z drugiej strony języki interpretowane, takie jak Python i Perl, mogą działać na dowolnej maszynie, o ile mają prawidłowy interpreter. Przechodzi wiersz po wierszu przez kod wysokiego poziomu, konwertując go na kod binarny.
Kod interpretowany jest zwykle wolniejszy niż kod skompilowany. Na przykład rozważmy pętlę. Zinterpretowany przekształci odpowiedni kod dla każdej iteracji pętli. Z drugiej strony skompilowany kod sprawi, że tłumaczenie będzie tylko jedno. Ponadto, ponieważ interpretery widzą tylko jedną linię naraz, nie są w stanie wykonać żadnego znaczącego kodu, takiego jak zmiana kolejności wykonywania instrukcji, takich jak kompilatory.
Poniżej przyjrzymy się przykładowi takiej optymalizacji -
Adding two numbers stored in memory. Ponieważ dostęp do pamięci może pochłaniać wiele cykli procesora, dobry kompilator wyda instrukcje, aby pobrać dane z pamięci i wykonać dodawanie tylko wtedy, gdy dane są dostępne. Nie będzie czekać iw międzyczasie wykona inne instrukcje. Z drugiej strony żadna taka optymalizacja nie byłaby możliwa podczas interpretacji, ponieważ interpreter w danym momencie nie jest świadomy całego kodu.
Ale wtedy języki interpretowane mogą działać na każdym komputerze, który ma poprawnego interpretera tego języka.
Czy Java jest kompilowana czy interpretowana?
Java próbowała znaleźć złoty środek. Ponieważ JVM znajduje się pomiędzy kompilatorem javac a sprzętem bazowym, kompilator javac (lub jakikolwiek inny kompilator) kompiluje kod Java w kodzie bajtowym, który jest rozumiany przez maszynę JVM specyficzną dla platformy. Następnie maszyna JVM kompiluje kod bajtowy w postaci binarnej przy użyciu kompilacji JIT (Just-in-time) w trakcie wykonywania kodu.
HotSpots
W typowym programie jest tylko niewielka część kodu, która jest wykonywana często i często to właśnie ten kod znacząco wpływa na wydajność całej aplikacji. Takie sekcje kodu nazywane sąHotSpots.
Jeśli jakaś sekcja kodu jest wykonywana tylko raz, kompilacja byłaby stratą czasu, a zamiast tego interpretacja kodu bajtowego byłaby szybsza. Ale jeśli sekcja jest sekcją aktywną i jest wykonywana wiele razy, maszyna JVM skompiluje ją zamiast tego. Na przykład, jeśli metoda jest wywoływana wiele razy, dodatkowe cykle potrzebne do skompilowania kodu zostaną przesunięte przez szybszy generowany plik binarny.
Co więcej, im bardziej maszyna JVM uruchamia określoną metodę lub pętlę, tym więcej informacji gromadzi w celu dokonania różnych optymalizacji, tak aby generowany był szybszy plik binarny.
Rozważmy następujący kod -
for(int i = 0 ; I <= 100; i++) {
System.out.println(obj1.equals(obj2)); //two objects
}
Jeśli ten kod zostanie zinterpretowany, interpreter wydedukuje dla każdej iteracji, że klasy obj1. Dzieje się tak, ponieważ każda klasa w Javie ma metodę .equals (), która jest rozszerzeniem z klasy Object i może być przesłonięta. Więc nawet jeśli obj1 jest łańcuchem dla każdej iteracji, odliczenie będzie nadal wykonywane.
Z drugiej strony faktycznie by się wydarzyło, gdyby JVM zauważył, że dla każdej iteracji obiekt obj1 ma klasę String, a zatem wygenerowałby bezpośrednio kod odpowiadający metodzie .equals () klasy String. W związku z tym nie będą wymagane żadne wyszukiwania, a skompilowany kod będzie wykonywał się szybciej.
Takie zachowanie jest możliwe tylko wtedy, gdy maszyna JVM wie, jak zachowuje się kod. W związku z tym czeka przed skompilowaniem pewnych sekcji kodu.
Poniżej kolejny przykład -
int sum = 7;
for(int i = 0 ; i <= 100; i++) {
sum += i;
}
Interpreter dla każdej pętli pobiera wartość „sumy” z pamięci, dodaje do niej „I” i zapisuje z powrotem w pamięci. Dostęp do pamięci jest kosztowną operacją i zwykle zajmuje wiele cykli procesora. Ponieważ ten kod działa wiele razy, jest to HotSpot. JIT skompiluje ten kod i dokona następującej optymalizacji.
Lokalna kopia „sum” byłaby przechowywana w rejestrze właściwym dla określonego wątku. Wszystkie operacje byłyby wykonywane na wartości w rejestrze, a po zakończeniu pętli wartość byłaby ponownie zapisywana w pamięci.
Co się stanie, jeśli inne wątki również uzyskują dostęp do zmiennej? Ponieważ aktualizacje lokalnej kopii zmiennej są wykonywane przez inny wątek, zobaczyliby nieaktualną wartość. W takich przypadkach wymagana jest synchronizacja wątków. Bardzo podstawowym prymitywem synchronizacji byłoby zadeklarowanie „sumy” jako zmiennej. Teraz, przed uzyskaniem dostępu do zmiennej, wątek opróżniłby swoje lokalne rejestry i pobrał wartość z pamięci. Po uzyskaniu do niego dostępu wartość jest natychmiast zapisywana w pamięci.
Poniżej znajduje się kilka ogólnych optymalizacji, które są wykonywane przez kompilatory JIT -
- Metoda inlining
- Eliminacja martwego kodu
- Heurystyka optymalizacji witryn połączeń
- Stałe składanie