JavaRush /Blog Java /Random-PL /Kompilowanie i wykonywanie aplikacji Java pod maską
Павел Голов
Poziom 34
Москва

Kompilowanie i wykonywanie aplikacji Java pod maską

Opublikowano w grupie Random-PL

Treść:

  1. Wstęp
  2. Kompilacja do kodu bajtowego
  3. Przykład kompilacji i wykonania programu
  4. Wykonywanie programu na maszynie wirtualnej
  5. Kompilacja just-in-time (JIT).
  6. Wniosek
Kompilowanie i wykonywanie aplikacji Java pod maską - 1

1. Wstęp

Cześć wszystkim! Dzisiaj chciałbym podzielić się wiedzą na temat tego co dzieje się pod maską JVM (Java Virtual Machine) po uruchomieniu aplikacji napisanej w Javie. Obecnie istnieją modne środowiska programistyczne, które pomagają uniknąć myślenia o wewnętrznych elementach JVM, kompilowaniu i wykonywaniu kodu Java, co może spowodować, że nowi programiści przeoczą te ważne aspekty. Jednocześnie podczas rozmów kwalifikacyjnych często pojawiają się pytania na ten temat, dlatego zdecydowałem się napisać artykuł.

2. Kompilacja do kodu bajtowego

Kompilowanie i wykonywanie aplikacji Java pod maską - 2
Zacznijmy od teorii. Pisząc dowolną aplikację tworzymy plik z rozszerzeniem .javai umieszczamy w nim kod w języku programowania Java. Taki plik zawierający kod czytelny dla człowieka nazywany jest plikiem kodu źródłowego . Gdy plik kodu źródłowego będzie gotowy, musisz go uruchomić! Ale na tym etapie zawiera informacje zrozumiałe tylko dla człowieka. Java to wieloplatformowy język programowania. Oznacza to, że programy napisane w języku Java można uruchamiać na dowolnej platformie, na której zainstalowany jest dedykowany system wykonawczy Java. System ten nazywa się wirtualną maszyną Java (JVM). Aby przetłumaczyć program z kodu źródłowego na kod zrozumiały dla JVM, należy go skompilować. Kod zrozumiały dla JVM nazywa się kodem bajtowym i zawiera zestaw instrukcji, które następnie wykona maszyna wirtualna. javacAby skompilować kod źródłowy do kodu bajtowego, w pakiecie JDK (Java Development Kit) znajduje się kompilator . Na wejściu kompilator przyjmuje plik z rozszerzeniem .java, zawierający kod źródłowy programu, a na wyjściu generuje plik z rozszerzeniem .class, zawierający kod bajtowy niezbędny do wykonania programu przez maszynę wirtualną. Po skompilowaniu programu do kodu bajtowego można go uruchomić przy użyciu maszyny wirtualnej.

3. Przykład kompilacji i wykonania programu

Załóżmy, że mamy prosty program zawarty w pliku Calculator.java, który pobiera 2 numeryczne argumenty wiersza poleceń i wypisuje wynik ich dodania:
class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
Aby skompilować ten program do kodu bajtowego, użyjemy kompilatora javacw wierszu poleceń:
javac Calculator.java
Po kompilacji otrzymujemy na wyjściu plik z kodem bajtowym Calculator.class, który możemy uruchomić na maszynie java zainstalowanej na naszym komputerze za pomocą komendy java z wiersza poleceń:
java Calculator 1 2
Zwróć uwagę, że po nazwie pliku podano 2 argumenty linii poleceń - cyfry 1 i 2. Po uruchomieniu programu w linii poleceń wyświetli się liczba 3. W powyższym przykładzie mieliśmy prostą klasę, która żyje samodzielnie . Ale co, jeśli klasa jest w jakimś pakiecie? Zasymulujmy następującą sytuację: utwórz katalogi src/ru/javarushi umieść tam naszą klasę. Teraz wygląda to tak (nazwę pakietu dodaliśmy na początku pliku):
package ru.javarush;

class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
Skompilujmy taką klasę za pomocą następującego polecenia:
javac -d bin src/ru/javarush/Calculator.java
W tym przykładzie wykorzystaliśmy dodatkową opcję kompilatora -d bin, która umieszcza skompilowane pliki w katalogu bino strukturze podobnej do katalogu src, z tym że katalog binmusi być wcześniej utworzony. Technikę tę stosuje się, aby uniknąć pomylenia plików kodu źródłowego z plikami kodu bajtowego. Przed uruchomieniem skompilowanego programu warto wyjaśnić koncepcję classpath. Classpathto ścieżka, względem której maszyna wirtualna będzie szukać pakietów i skompilowanych klas. Oznacza to, że w ten sposób informujemy maszynę wirtualną, które katalogi w systemie plików są korzeniem hierarchii pakietów Java. Classpathmożna określić podczas uruchamiania programu przy użyciu flagi -classpath. Program uruchamiamy za pomocą polecenia:
java -classpath ./bin ru.javarush.Calculator 1 2
W tym przykładzie wymagaliśmy pełnej nazwy klasy, łącznie z nazwą pakietu, w którym się ona znajduje. Ostateczne drzewo plików wygląda następująco:
├── src
│     └── ru
│          └── javarush
│                  └── Calculator.java
└── bin
      └── ru
           └── javarush
                   └── Calculator.class

4. Wykonanie programu przez maszynę wirtualną

Uruchomiliśmy więc napisany program. Ale co się stanie, gdy skompilowany program zostanie uruchomiony przez maszynę wirtualną? Najpierw zastanówmy się, co oznaczają pojęcia kompilacji i interpretacji kodu. Kompilacja to tłumaczenie programu napisanego w języku źródłowym wysokiego poziomu na równoważny program w języku niskiego poziomu, podobnym do kodu maszynowego. Interpretacja to analiza, przetwarzanie i natychmiastowe wykonanie programu źródłowego lub żądania (w przeciwieństwie do kompilacji, w której program jest tłumaczony bez jego wykonywania), wykonywana przez operatora po instrukcji (polecenie po wierszu, wiersz po wierszu). Język Java ma zarówno kompilator ( javac), jak i interpreter, który jest maszyną wirtualną, która konwertuje kod bajtowy na kod maszynowy wiersz po wierszu i natychmiast go wykonuje. Tym samym, gdy uruchomimy skompilowany program, maszyna wirtualna zaczyna go interpretować, czyli linijka po linii konwersji kodu bajtowego na kod maszynowy, a także jego wykonanie. Niestety, interpretacja czystego kodu bajtowego jest dość długim procesem i spowalnia Javę w porównaniu z konkurencją. Aby tego uniknąć wprowadzono mechanizm przyspieszający interpretację kodu bajtowego przez maszynę wirtualną. Mechanizm ten nazywa się kompilacją just-in-time (JITC).

5. Kompilacja just-in-time (JIT).

W uproszczeniu mechanizm kompilacji Just-In-Time jest następujący: jeśli w programie znajdują się części kodu, które są wykonywane wielokrotnie, to można je raz skompilować do kodu maszynowego, aby przyspieszyć ich wykonanie w przyszłości. Po skompilowaniu takiej części programu do kodu maszynowego, przy każdym kolejnym wywołaniu tej części programu, maszyna wirtualna od razu wykona skompilowany kod maszynowy, zamiast go interpretować, co w naturalny sposób przyspieszy wykonanie programu. Przyspieszenie programu osiąga się poprzez zwiększenie zużycia pamięci (musimy gdzieś przechowywać skompilowany kod maszynowy!) oraz zwiększenie czasu kompilacji podczas wykonywania programu. Kompilacja JIT to dość złożony mechanizm, więc przejdźmy od góry. Istnieją 4 poziomy kompilacji JIT kodu bajtowego do kodu maszynowego. Im wyższy poziom kompilacji, tym jest ona bardziej złożona, ale jednocześnie wykonanie takiej sekcji będzie szybsze niż sekcji o niższym poziomie. JIT – kompilator decyduje, jaki poziom kompilacji ustawić dla każdego fragmentu programu na podstawie częstotliwości wykonywania tego fragmentu. Pod maską JVM wykorzystuje 2 kompilatory JIT - C1 i C2. Kompilator C1 nazywany jest także kompilatorem klienta i potrafi kompilować kod tylko do trzeciego poziomu. Kompilator C2 odpowiada za czwarty, najbardziej złożony i najszybszy poziom kompilacji.
Kompilowanie i wykonywanie aplikacji Java pod maską - 3
Z powyższego możemy wywnioskować, że w przypadku prostych aplikacji klienckich bardziej opłaca się skorzystać z kompilatora C1, ponieważ w tym przypadku ważne jest dla nas, jak szybko aplikacja się uruchomi. Aplikacje serwerowe, długowieczne, mogą uruchamiać się dłużej, ale w przyszłości muszą szybko działać i wykonywać swoje funkcje - tutaj kompilator C2 jest dla nas odpowiedni. Uruchamiając program Java na maszynie JVM x32, możemy ręcznie określić, którego trybu chcemy używać, używając flag -clienti -server. Po określeniu tej flagi -clientmaszyna JVM nie będzie wykonywać skomplikowanych optymalizacji kodu bajtowego, co przyspieszy czas uruchamiania aplikacji i zmniejszy ilość zużywanej pamięci. Po określeniu flagi -serveruruchomienie aplikacji zajmie więcej czasu ze względu na złożone optymalizacje kodu bajtowego i zużyje więcej pamięci do przechowywania kodu maszynowego, ale w przyszłości program będzie działał szybciej. W wersji JVM x64 flaga -clientjest ignorowana i domyślnie używana jest konfiguracja serwera aplikacji.

6. Wniosek

Na tym kończę mój krótki przegląd sposobu kompilowania i wykonywania aplikacji Java. Główne punkty:
  1. Kompilator javac konwertuje kod źródłowy programu na kod bajtowy, który można wykonać na dowolnej platformie, na której zainstalowana jest wirtualna maszyna Java;
  2. Po kompilacji JVM interpretuje powstały kod bajtowy;
  3. Aby przyspieszyć działanie aplikacji Java, JVM wykorzystuje mechanizm kompilacji Just-In-Time, który konwertuje najczęściej wykonywane sekcje programu na kod maszynowy i przechowuje je w pamięci.
Mam nadzieję, że ten artykuł pomógł Ci lepiej zrozumieć działanie naszego ulubionego języka programowania. Dziękuję za przeczytanie, krytyka mile widziana!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION