JavaRush /Blog Java /Random-PL /Jak klasy są ładowane do JVM
Aleksandr Zimin
Poziom 1
Санкт-Петербург

Jak klasy są ładowane do JVM

Opublikowano w grupie Random-PL
Po zakończeniu najtrudniejszej części pracy programisty i napisaniu aplikacji „Hello World 2.0” pozostaje tylko skompletować pakiet dystrybucyjny i przekazać go klientowi lub przynajmniej serwisowi testowemu. W dystrybucji wszystko jest tak jak powinno być, a kiedy uruchamiamy nasz program, na scenę wchodzi Wirtualna Maszyna Java. Nie jest tajemnicą, że maszyna wirtualna odczytuje polecenia zawarte w plikach klas w postaci kodu bajtowego i tłumaczy je jako instrukcje dla procesora. Proponuję trochę zrozumieć schemat kodu bajtowego dostającego się do maszyny wirtualnej.

Ładowarka klas

Służy do dostarczania skompilowanego kodu bajtowego do JVM, który zwykle jest przechowywany w plikach z rozszerzeniem .class, ale można go również pozyskać z innych źródeł, na przykład pobrać przez sieć lub wygenerować przez samą aplikację. Jak klasy są ładowane w JVM - 1Zgodnie ze specyfikacją Java SE, aby uruchomić kod w JVM, należy wykonać trzy kroki:
  • ładowanie kodu bajtowego z zasobów i tworzenie instancji klasyClass

    Obejmuje to wyszukiwanie żądanej klasy wśród wcześniej załadowanych, uzyskanie kodu bajtowego w celu załadowania i sprawdzenia jego poprawności, utworzenie instancji klasy Class(do pracy z nią w czasie wykonywania) oraz załadowanie klas nadrzędnych. Jeśli klasy nadrzędne i interfejsy nie zostały załadowane, wówczas dana klasa jest uważana za niezaładowaną.

  • wiązanie (lub łączenie)

    Zgodnie ze specyfikacją etap ten dzieli się na trzy kolejne etapy:

    • Weryfikacja , sprawdzana jest poprawność otrzymanego kodu bajtowego.
    • Przygotowanie , przydzielenie pamięci RAM dla pól statycznych i inicjowanie ich wartościami domyślnymi (w tym przypadku jawna inicjalizacja, jeśli występuje, następuje już na etapie inicjalizacji).
    • Rozdzielczość , rozdzielczość dowiązań symbolicznych typów, pól i metod.
  • inicjowanie odebranego obiektu

    tutaj, w przeciwieństwie do poprzednich akapitów, wszystko wydaje się jasne, co powinno się wydarzyć. Byłoby oczywiście interesujące dowiedzieć się, jak dokładnie to się dzieje.

Wszystkie te kroki są wykonywane sekwencyjnie z następującymi wymaganiami:
  • Klasa musi być w pełni załadowana przed połączeniem.
  • Klasa musi zostać w pełni przetestowana i przygotowana przed jej zainicjowaniem.
  • Błędy rozpoznawania łączy występują podczas wykonywania programu, nawet jeśli zostały wykryte na etapie łączenia.
Jak wiesz, Java implementuje leniwe (lub leniwe) ładowanie klas. Oznacza to, że ładowanie klas pól referencyjnych załadowanej klasy nie zostanie wykonane, dopóki aplikacja nie napotka jawnego odniesienia do nich. Innymi słowy, rozwiązywanie dowiązań symbolicznych jest opcjonalne i domyślnie nie występuje. Jednak implementacja JVM może również wykorzystywać ładowanie klas energetycznych, tj. wszystkie dowiązania symboliczne należy natychmiast wziąć pod uwagę. Właśnie w tym miejscu ma zastosowanie ostatni wymóg. Warto również zauważyć, że rozdzielczość dowiązań symbolicznych nie jest powiązana z żadnym etapem ładowania klasy. Ogólnie rzecz biorąc, każdy z tych etapów jest dobry do zbadania; spróbujmy rozgryźć pierwszy, a mianowicie ładowanie kodu bajtowego.

Rodzaje modułów ładujących Java

W Javie istnieją trzy standardowe programy ładujące, z których każdy ładuje klasę z określonej lokalizacji:
  1. Bootstrap to podstawowy program ładujący, zwany także Pierwotnym ClassLoaderem.

    ładuje standardowe klasy JDK z archiwum rt.jar

  2. Rozszerzenie ClassLoader – moduł ładujący rozszerzenia.

    ładuje klasy rozszerzeń, które domyślnie znajdują się w katalogu jre/lib/ext, ale można je ustawić za pomocą właściwości systemowej java.ext.dirs

  3. System ClassLoader – moduł ładujący system.

    ładuje klasy aplikacji zdefiniowane w zmiennej środowiskowej CLASSPATH

Java korzysta z hierarchii modułów ładujących klasy, gdzie korzeń jest oczywiście głównym. Następny jest moduł ładujący rozszerzeń, a następnie moduł ładujący system. Naturalnie każdy moduł ładujący przechowuje wskaźnik do rodzica, aby móc delegować mu ładowanie w przypadku, gdy sam nie jest w stanie tego zrobić.

Klasa abstrakcyjna ClassLoader

Każdy moduł ładujący, z wyjątkiem podstawowego, jest potomkiem klasy abstrakcyjnej java.lang.ClassLoader. Na przykład implementacją modułu ładującego rozszerzenie jest klasa sun.misc.Launcher$ExtClassLoader, a modułem ładującym system jest klasa sun.misc.Launcher$AppClassLoader. Podstawowy moduł ładujący jest natywny i jego implementacja jest zawarta w JVM. Każda klasa, która się rozszerza, java.lang.ClassLoadermoże zapewnić własny sposób ładowania klas za pomocą blackjacka i tych samych. Aby to zrobić, konieczne jest przedefiniowanie odpowiednich metod, które w tej chwili mogę rozważać jedynie powierzchownie, ponieważ Nie zrozumiałem szczegółowo tego problemu. Tutaj są:
package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)jedna z niewielu metod publicznych, będąca punktem wejścia do ładowania klas. Jego implementacja sprowadza się do wywołania innej chronionej metody loadClass(String name, boolean resolve), którą należy nadpisać. Jeśli spojrzysz na dokument Javadoc tej chronionej metody, możesz zrozumieć coś takiego: jako dane wejściowe dostarczane są dwa parametry. Jedna to binarna nazwa klasy (lub w pełni kwalifikowana nazwa klasy), która ma zostać załadowana. Nazwa klasy jest podana wraz z listą wszystkich pakietów. Drugi parametr to flaga określająca, czy wymagane jest rozpoznawanie łącza symbolicznego. Domyślnie jest to false , co oznacza, że ​​używane jest leniwe ładowanie klas. Dalej zgodnie z dokumentacją w domyślnej implementacji metody wykonywane jest wywołanie findLoadedClass(String name), które sprawdza, czy klasa nie została już wcześniej załadowana i jeżeli tak, zwraca referencję do tej klasy. W przeciwnym razie zostanie wywołana metoda ładowania klasy nadrzędnego programu ładującego. Jeśli żaden z programów ładujących nie mógł znaleźć załadowanej klasy, każdy z nich, postępując w odwrotnej kolejności, spróbuje znaleźć i załadować tę klasę, zastępując metodę findClass(String name). Zostanie to omówione bardziej szczegółowo w rozdziale „Schemat ładowania klas”. I wreszcie, co nie mniej ważne, po załadowaniu klasy, w zależności od flagi rozwiązania , zostanie podjęta decyzja, czy ładować klasy poprzez dowiązania symboliczne. Wyraźnym przykładem jest to, że etap Rozdzielczości można wywołać podczas etapu ładowania klasy. Odpowiednio, rozszerzając klasę ClassLoaderi zastępując jej metody, niestandardowy moduł ładujący może zaimplementować własną logikę dostarczania kodu bajtowego do maszyny wirtualnej. Java obsługuje również koncepcję „bieżącego” modułu ładującego klasy. Bieżący moduł ładujący to ten, który załadował aktualnie wykonywaną klasę. Każda klasa wie, jakim programem ładującym została załadowana, i możesz uzyskać tę informację, wywołując jej funkcję String.class.getClassLoader(). Dla wszystkich klas aplikacji „bieżącym” modułem ładującym jest zwykle program ładujący systemowy.

Trzy zasady ładowania klas

  • Delegacja

    Żądanie załadowania klasy jest przekazywane do nadrzędnego programu ładującego, a próba załadowania samej klasy jest podejmowana tylko wtedy, gdy nadrzędny program ładujący nie był w stanie znaleźć i załadować klasy. Takie podejście pozwala ładować klasy programem ładującym możliwie najbliżej podstawowego. Zapewnia to maksymalną widoczność klasy. Każdy moduł ładujący rejestruje klasy, które zostały przez niego załadowane, umieszczając je w swojej pamięci podręcznej. Zbiór tych klas nazywany jest zasięgiem.

  • Widoczność

    Program ładujący widzi tylko „swoje” klasy i klasy „rodzica” i nie ma pojęcia, jakie klasy zostały załadowane przez jego „dziecko”.

  • Wyjątkowość

    Klasę można załadować tylko raz. Mechanizm delegowania dba o to, aby moduł ładujący inicjujący ładowanie klasy nie przeciążał klasy, która została wcześniej załadowana do maszyny JVM.

Dlatego pisząc swój bootloader, programista powinien kierować się tymi trzema zasadami.

Schemat ładowania klas

Gdy nastąpi wywołanie ładowania klasy, ta klasa jest przeszukiwana w pamięci podręcznej już załadowanych klas bieżącego modułu ładującego. Jeżeli żądana klasa nie została wcześniej załadowana, zasada delegowania przekazuje kontrolę do nadrzędnego modułu ładującego, który znajduje się o jeden poziom wyżej w hierarchii. Nadrzędny moduł ładujący próbuje również znaleźć żądaną klasę w swojej pamięci podręcznej. Jeśli klasa została już załadowana i moduł ładujący zna jej lokalizację, wówczas Classzostanie zwrócony obiekt tej klasy. Jeśli nie, wyszukiwanie będzie kontynuowane, aż dotrze do podstawowego programu ładującego. Jeżeli w baseloaderze nie ma informacji o wymaganej klasie (czyli nie został jeszcze załadowany), kod bajtowy tej klasy zostanie wyszukany w lokalizacji klas, o których dany program ładujący wie, a jeśli klasa nie może zostanie załadowany, sterowanie powróci do modułu ładującego podrzędnego, który spróbuje załadować ze znanych mu źródeł. Jak wspomniano powyżej, lokalizacją klas dla programu ładującego bazowego jest biblioteka rt.jar, dla programu ładującego rozszerzenia - katalog z rozszerzeniami jre/lib/ext, dla systemowego - CLASSPATH, dla użytkownika może to być coś innego . Zatem postęp ładowania klas przebiega w przeciwnym kierunku - od modułu ładującego root do bieżącego. Po znalezieniu kodu bajtowego klasy, klasa jest ładowana do maszyny JVM i uzyskiwana jest instancja typu Class. Jak łatwo zauważyć, opisany schemat ładowania jest podobny do powyższej implementacji metody loadClass(String name). Poniżej możesz zobaczyć ten diagram na schemacie.
Jak klasy są ładowane w JVM - 2

Jako podsumowanie

Na pierwszych etapach nauki języka nie ma szczególnej potrzeby zrozumienia sposobu ładowania klas w Javie, jednak znajomość tych podstawowych zasad pomoże Ci uniknąć rozpaczy w przypadku napotkania błędów takich jak ClassNotFoundExceptionlub NoClassDefFoundError. Cóż, lub przynajmniej z grubsza zrozumieć, na czym polega źródło problemu. Zatem wyjątek ClassNotFoundExceptionwystępuje, gdy klasa jest ładowana dynamicznie podczas wykonywania programu, gdy programy ładujące nie mogą znaleźć wymaganej klasy ani w pamięci podręcznej, ani wzdłuż ścieżki klas. Jednak błąd NoClassDefFoundErrorjest bardziej krytyczny i występuje, gdy wymagana klasa była dostępna podczas kompilacji, ale nie była widoczna podczas wykonywania programu. Może się to zdarzyć, jeśli program zapomni dołączyć używaną bibliotekę. Otóż ​​samo zrozumienie zasad budowy narzędzia, którym się posługujesz w swojej pracy (niekoniecznie wyraźne i szczegółowe zanurzenie się w jego głąb) dodaje pewnej przejrzystości zrozumieniu procesów zachodzących wewnątrz tego mechanizmu, który w z kolei prowadzi do pewnego wykorzystania tego narzędzia.

Źródła

Jak ClassLoader działa w Javie Ogólnie rzecz biorąc, bardzo przydatne źródło z przystępną prezentacją informacji. Ładowanie klas, ClassLoader Dość długi artykuł, ale z naciskiem na to, jak stworzyć własną implementację modułu ładującego za pomocą tych samych. ClassLoader: dynamiczne ładowanie klas Niestety ten zasób nie jest już dostępny, ale tam znalazłem najbardziej zrozumiały diagram ze schematem ładowania klas, więc nie mogę powstrzymać się od jego dodania. Specyfikacja Java SE: Rozdział 5. Ładowanie, łączenie i inicjowanie
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION