Learn · concepts
Czym jest ładowacz klas Java?
Ładowacz klas wyszukuje i ładuje klasy Java do JVM w czasie działania programu. Oto jak one działają, dlaczego ważna jest hierarchia i dlaczego mody od nich zależą.
Ładowacz klas to część środowiska uruchomieniowego Java, która znajduje plik klasy i ładuje go do JVM. Za każdym razem, gdy Twój kod odwołuje się do typu po raz pierwszy, JVM pyta ładowacz klas: "Znajdź mi tę klasę." Ładowacz lokalizuje bytecode, odczytuje go, a JVM definiuje klasę w pamięci. Ten mechanizm jest niewidoczny w normalnych programach Java, ale stanowi dokładny punkt zaczepienia, który wykorzystują frameworki modyfikacji do łatania Minecrafta.
Co tak naprawdę robi ładowacz klas
Ładowacz klas przekształca nazwę klasy w używaną klasę w działającym JVM. Prosisz o typ, ładowacz znajduje pasujący plik .class (lub generuje go), weryfikuje i łączy go, a klasa jest gotowa do użycia.
Praca odbywa się w trzech krokach:
- Znajdź — Ładowacz lokalizuje bytecode dla nazwanej klasy (z pliku JAR, dysku, sieci lub generowanego w locie).
- Zweryfikuj i połącz — JVM sprawdza, czy bytecode jest poprawny i rozwiązuje odwołania do innych klas.
- Użyj — Klasa jest gotowa i Twój program może ją instancjować.
Dlatego programy Java mogą dynamicznie ładować kod w czasie działania, który nigdy nie został wspomniany w czasie kompilacji. Nazwa klasy wystarczy ładowaczowi, aby go odnaleźć.
Hierarchia ładowaczy klas
Ładowacze klas są ułożone w hierarchię nadrzędną, a większość z nich najpierw pyta swojego nadrzędnego. Standardowa aplikacja Java ma trzy wbudowane ładowacze klas:
| Ładowacz | Ładowania | Cel |
|---|---|---|
| Bootstrap | Podstawowe klasy środowiska uruchomieniowego Java | Zapewnia, że podstawowe typy, takie jak java.lang.Object, są zawsze takie same |
| Platforma | Standardowe moduły biblioteczne spoza rdzenia | Moduły i rozszerzenia Java |
| Aplikacja | Własne klasy Twojego programu | Ścieżka klas (classpath) określona przez Twój program |
Gdy ładowacz aplikacji jest proszony o klasę, zazwyczaj najpierw deleguje zapytanie wyżej. Pyta ładowacz platformy, który pyta ładowacz bootstrap, który próbuje ją załadować. Tylko wtedy, gdy żaden rodzic nie może jej załadować, dziecko próbuje. To jest model delegacji, który zapobiega dwukrotnemu ładowaniu tej samej klasy pod różnymi nazwami.
Tożsamość klasy i jak to działa
Klasa jest identyfikowana zarówno po jej nazwie jak i po ładowaczu, który ją zdefiniował. Dwa różne ładowacze mogą załadować klasę o nazwie com.example.MyClass, a JVM traktuje je jako dwa zupełnie różne typy. Brzmi to obscure, ale to podstawa wszystkiego ciekawego, co Java potrafi robić.
Bootstrap Loader: java.lang.Object (the core Object)
Application Loader: java.lang.Object (if somehow re-loaded by app loader)
→ Two different types, same name
→ This breaks your program, which is why delegation prevents it
Niestandardowe ładowacze klas
Programy mogą definiować własne ładowacze klas. Niestandardowy ładowacz może odczytywać bytecode z dowolnego miejsca: pliku JAR, sieci, bazy danych lub bytecode, który został wygenerowany lub przekształcony w locie. Dzieje się to w następujący sposób:
- Systemy wtyczek ładują kod dynamicznie, nie wiedząc o nim w czasie kompilacji.
- Serwery aplikacji (Tomcat, JBoss) uruchamiają wiele aplikacji w tej samej JVM, każda z własnym ładowaczem klas.
- Mody Minecraft poprawiają klasy gry podczas ich ładowania.
Dlaczego to ma znaczenie dla modów Minecraft
Mody istnieją dzięki niestandardowemu ładowaniu klas. Ładowacz modów dla Minecrafta uruchamia grę za pomocą własnego, niestandardowego ładowacza klas, aby móc zobaczyć i modyfikować klasy gry podczas ich ładowania. To punkt przechwytywania, w którym dzieje się magia.
Kiedy ładowacz znajduje się pomiędzy dyskiem a JVM, może przekształcić klasę tuż przed zdefiniowaniem jej przez JVM:
Disk: obfuscated Minecraft class
↓
Mod Loader (custom classloader)
↓ [bytecode transformation happens here]
↓
JVM: defines the patched class
↓
Memory: game runs with mods active
Mod może przepisać metodę gry, wstrzyknąć nowe zachowanie, dodać pola, wszystko to bez dotykania pliku JAR gry na dysku. Klasa, którą ostatecznie uruchamia JVM, to ta, którą zmodyfikował framework moda. Dokładnie tak działają Fabric, Forge i inne ładowacze modów w Minecraft.
Ładowanie leniwe
Klasy nie są wszystkie ładowane podczas uruchamiania. JVM ładuje każdą klasę po raz pierwszy, gdy jest ona faktycznie używana. Nazywa się to ładowaniem leniwym:
Program starts → JVM doesn't load String class yet
User calls System.out.println() → String is needed → Loader loads it
Oznacza to, że ładowacz klas musi znaleźć i załadować tylko klasy, które faktycznie dotyka twój kod. Nieużywane klasy nigdy nie wchodzą do pamięci. Dlatego uruchomienie Minecrafta może trochę potrwać (mody dodają wiele klas), ale nie zamarza na zawsze.
FAQ
Podczas pierwszego użycia, a nie podczas uruchamiania. Po raz pierwszy, gdy Twój kod dotyka typu, JVM prosi ładowacz klas o znalezienie i zdefiniowanie go. Nazywa się to leniwym ładowaniem i dlatego nieużywany kod nigdy nie trafia do pamięci.
Ładowacz klas najpierw prosi swojego rodzica o załadowanie klasy, zanim spróbuje to zrobić sam. Łańcuch rodziców prowadzi aż do ładowacza rozruchowego. Zapobiega to dwukrotnemu załadowaniu tej samej klasy pod tą samą nazwą przez różnych ładowaczy.
Tak, jeśli zdefiniują je różne klasy ładowania. JVM identyfikuje klasę zarówno po nazwie, jak i po ładującym ją loaderze, więc klasy o tej samej nazwie z oddzielnych loaderów traktowane są jako odrębne typy.
Aby przechwytywać klasy podczas ich ładowania i przekształcać je w pamięci. Niestandardowy ładowacz umieszcza się pomiędzy dyskiem a JVM, modyfikując bytecode przed zdefiniowaniem klasy przez JVM. Właśnie tak mody poprawiają Minecrafta bez modyfikowania pliku jar gry.