Class Loadery to mechanizm odpowiedzialny za ładowanie klas. Pełnią one bardzo istotną rolę w trakcie działania Wirtualnej Maszyny Javy (JVM). Class Loadery to klasy napisane w Javie… No dobrze, to kto załadował pierwszą klasę? Zapraszam do wpisu, aby się tego dowiedzieć!

ClassLoader

Class Loader jest mechanizmem, który ładuje klasy z postaci binarnej do postaci wykonywalnej na Wirtualnej Maszynie Javy. Klasy te ładowane są dynamicznie, co oznacza, że są dostarczone tylko wtedy, gdy są potrzebne, na podstawie nazwy wraz z pakietem/ścieżką. W Javie Class Loadery występują w hierarchii.

Hierarchia

Poniższy obrazek prezentuje hierarchię Class Loaderów:

Bootstrap Class Loader

Pierwszym Class Loaderem 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. Znajduje się tam między innymi rt.jar, który zawiera podstawowe klasy, takie jak String czy kolekcje. Ponadto ładowane są Javowe Class Loadery. Każdy kolejny typ Class Loadera jest już napisany w Javie.

Extension Class Loader

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

System Class Loader

Ostatnim obowiązkowym Class Loaderem jest System Class Loader (często nazywany też Application Class Loader). Ładuje on wszystkie klasy, które znajdują się na tak zwanym classpathie. Classpath to wartość, która zawiera informacje o klasach i ich ścieżkach, jakie mają być załadowane do JVM. Classpath 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 przechowujemy ścieżki do plików .class oraz .jar. Ścieżki rozdzielamy znakiem : dla systemów Unix/Linux/macOS i ; dla systemów Windows. Przykładowa zmienna środowiskowa to: CLASSPATH=path/to/classes:path/to/jars.

Wiersz poleceń

Podczas uruchamiania programu z wiersza poleceń (java Run) można wskazać własny classpath. Odbywa się to poprzez użycie przełączników -cp lub -classpath. Należy pamiętać, że tak przekazane wartości zmiennej classpath 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 przypadku archiwum, informacje o classpathie powinny znajdować się wewnątrz pliku manifest (tym razem podajemy ścieżki względne, które rozdzielamy spacjami):

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

Konflikt nazw

Jeśli w CLASSPATH znajdują się dwie klasy o identycznych nazwach w tych samych pakietach, to tylko pierwsza z nich na classpathie zostanie załadowana.

Sprawdzamy

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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ć, że Class Loader dla klasy String jest tak zwanym Nullowym Class Loaderem, ponieważ nie jest on napisany w Javie (jest natywny). Natomiast dwa pozostałe to już klasy Javowe.

Hierarchiczność

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

ClassNotFoundException vs NoClassDefFoundError

Dwa wyjątki związane z Class Loaderami to ClassNotFoundException oraz NoClassDefFoundError.

Wyjątek ClassNotFoundException występuje wtedy, gdy Class Loader nie znajduje klasy na classpathie:

1
2
3
4
5
6
7
@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 inicjalizacji (np. w bloku statycznym) wystąpił błąd i jej definicja nie mogła zostać utworzona:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
void shouldThrowNoClassDefFoundErrorWhenCannotLoadClass() {
// Given
MyBrokenWrapper wrapper = new MyBrokenWrapper();
// When
Executable executable = wrapper::create;
// Then
assertThrows(NoClassDefFoundError.class, executable);
}

class MyBrokenClass {

static int value;

static {
// Ta operacja kończy się błędem inicjalizacji
value = 1/0;
}

}

class MyBrokenWrapper {

MyBrokenClass create() {
try {
// Pierwsza próba użycia MyBrokenClass spowoduje błąd inicjalizacji
new MyBrokenClass();
} catch (ExceptionInInitializerError e) {
// Wyjątek jest łapany, ale klasa jest już "zepsuta" w JVM
}
// Druga próba użycia rzuca NoClassDefFoundError
return new MyBrokenClass();
}

}

Github

Całość jak zawsze na GitHubie.