Domyślne Class Loadery w Javie

ClassLoader’y są mechanizmem odpowiedzialnym za ładowanie klas. Pełnią one bardzo istotną rolę w trakcie działania wirtualnej maszyny Javy. ClassLoader’y są to klasy napisane w Javie, no dobra to kto załadował pierwszą klasę? Zapraszam do wpisu, aby się tego dowiedzieć!

ClassLoader

ClassLoader jest mechanizmem, który ładuje klasy z postaci binarnej do postaci wykonywalnej na wirtualnej maszynie Javy. Klasy te ładowane są dynamicznie co oznacza, iż dostarczone są tylko wtedy, gdy są potrzebne na podstawie nazwy wraz z pakietem/ścieżką. W Javie ClassLoader’y występują w hierarchii.

Hierarchia

Poniższy obrazek prezentuje hierachię ClassLoader’ów:

Bootstrap Class Loader

Pierwszym Class Loader’em w hierarchii jest tak zwany Bootstrap/Null/Primordial Class Loader. Jest to Class Loader napisany w kodzie natywnym, którego zadaniem jest dostarczenie wszystkich elementów z folderu $JAVA_HOME/lib/*.jar. Znajduję się tam między innymi rt.jar, które zawiera podstawowe klasy jak String czy kolekcje. Ponadto ładowane są Javowe Class Loader’y. Każdy kolejny typ Class Loader’a jest już napisany w Javie.

Extension Class Loader

Kolejny Class Loader odpowiedzialny jest za dostarczenie dodatkowych narzędzi, które znajdują się w folderze $JAVA_HOME/lib/ext/*.jar lub w każdym innym dowolnym folderze zdefiniowanym w zmiennej systemowej java.ext.dirs. W folderze ext możemy znaleźć między innymi binaria projektu Nashorn (który jest deprecated od Javy 11). Extension Class Loader jest drugim w hierarchii Class Loader’em.

System Class Loader

Ostatnim obowiązkowym Class Loader’em jest System Class Loader. Ładuje on wszystkie klasy, które znajdują się na tak zwanym class path’ie. Class Path jest to wartość, która zawiera informacje o klasach i ich ścieżkach jakie mają być załadowane do JVM’a. Class Path można ustawić na kilka sposobów:

  • Zmienna środowiskowa
  • Wiersz poleceń
  • Manifest

Zmienna środowiskowa

Zmienną środowiskową odpowiedzialną za dostarczenie ścieżek do klas jest CLASSPATH. W tej zmiennej środowiskowej przechowujemy ścieżki do plików .class oraz .jar. Ścieżki rozdzielamy znakiem : dla systemów Unix i ; dla systemów Windows. Przykładowa zmienna środowiskowa CLASSPATH=path/to/classes;path/to/jars.

Wiersz poleceń

Podczas uruchamiania programu z wiersza poleceń java Run można wskazać własny class path. Wykonuje się to poprzez użycie przełączników -cp lub -classpath. Należy pamiętać o tym, iż tak przekazane wartości zmiennej class path nadpisują zmienną globalną CLASSPATH. Domyślny CLASSPATH to folder, w którym został uruchomiony kod.

Manifest

Jeśli uruchamiamy aplikację jako archiwum java -jar wtedy wartości -cp, -classpath oraz CLASSPATH zostaną zignorowane. W przypadków archiwum informacje o class path’ie powinny znajdować się wewnątrz pliku manifest (tym razem podajemy ścieżki względne, które rozdzielamy spacjami):

Main-Class: pl.codecouple.Runner
Class-Path: lib/code.jar lib/couple.jar

Konflikt nazw

Jeśli w CLASSPATH znajdują się dokładnie dwie takie same nazwy klas w tych samych pakietach to tylko pierwsza z nich na class path’ie zostanie załadowana.

Sprawdzamy

Jeśli w naszym kodzie mamy widoczność na daną klasę to możemy pobrać z niej Class Loader, przez który została załadowana:

@Test
void shouldReturnNullClassLoaderWhenLoadStringClass() {
    // Given
    Class<String> stringClass = String.class;
    // When
    ClassLoader classLoader = stringClass.getClassLoader();
    // Then
    assertThat(classLoader).isNull();
}

@Test
void shouldReturnExtensionClassLoaderWhenLoadExtensionClass() {
    // Given
    Class<Shell> extensionClass = Shell.class;
    // When
    ClassLoader classLoader = extensionClass.getClassLoader();
    // Then
    assertThat(classLoader).isNotNull();
    assertThat(classLoader).isInstanceOf(getExtensionClassLoaderClass());
}

@Test
void shouldReturnApplicationClassLoaderWhenLoadApplicationClass() {
    // Given
    Class<MyClass> applicationClass = MyClass.class;
    // When
    ClassLoader classLoader = applicationClass.getClassLoader();
    // Then
    assertThat(classLoader).isNotNull();
    assertThat(classLoader).isInstanceOf(getApplicationClassLoaderClass());
}

W wyniku widać, iż Class Loader dla klasy String jest tak zwanym Nullowym Class Loader’em, ponieważ nie jest on napisany w Javie. Natomiast dwa pozostałe to już klasy Javowe.

Hierarchiczność

Class Loader’y komunikują się ze sobą w sposób hierarchiczny. Jeśli w aktualnym Class Loader’rze ktoś zażąda klasy, która nie została jeszcze załadowana to Class Loader odpytuje swojego ojca (Class Loader’a wyżej w hierarchii). Jeśli ojciec także nie posiada danego zasobu hierarchia pnie się w górę. Jeśli ostatni w hierachii Class Loader (w tym przypadku Bootstrap) nie załaduje żądanej klasy, to wtedy aktualny Class Loader próbuje ją załadować. Jeśli ładowanie się nie powiodło to występuje wyjątek ClassNotFoundException lub NoClassDefFoundError.

ClassNotFoundException vs NoClassDefFoundError

Dwa wyjątki związane z Class Loader’ami to ClassNotFoundException oraz NoClassDefFoundError. Wyjątek ClassNotFoundException występuje wtedy, gdy Class Loader nie znajduje klasy na class path’ie:

@Test
void shouldThrowClassNotFoundExceptionWhenCannotFindClassOnClassPath() {
    // When
    Executable executable = () -> Class.forName("random.name");
    // Then
    assertThrows(ClassNotFoundException.class, executable);
}

NoClassDefFoundError występuje wtedy, gdy klasa została załadowana przez Class Loader, ale podczas tworzenia nowej instancji lub ładowania jej wystąpił błąd (ten test był cięższy do napisania):

@Test
void shouldThrowNoClassDefFoundErrorWhenCannotLoadClass() {
    // Given
    MyBrokenWrapper wrapper = new MyBrokenWrapper();
    // When
    Executable executable = wrapper::create;
    // Then
    assertThrows(NoClassDefFoundError.class, executable);
}

class MyBrokenClass {

    static int value;

    static {
        value = 1/0;
    }

}

class MyBrokenWrapper {

    MyBrokenClass create() {
        try {
            new MyBrokenClass();
        } catch (ExceptionInInitializerError e) {

        }
        return new MyBrokenClass();
    }

}

Github

Całość jak zawsze na Github’ie.

  • Grzegorz Sancewicz

    Cześć. Lubię waszego bloga 🙂

    W Javie od wersji 9 trochę się pozmieniało w kontekście ClassLoaderów.

    http://openjdk.java.net/jeps/261#Class-loaders

    Myślę że warto zaznaczyć to w tym poście i stworzyć jego kontynuację uwzględniającą zmiany. Sam zamierzam pewne ciekawostki opisać na swoim blogu, lecz jest to jeszcze WIP.
    Pozdrowienia.

    • CodeCouple.pl

      Witaj,

      Super informacje! Jak najbardziej uzupełnimy.