Po co powstały
Żeby nie wiązać się ze sposobem dostarczania do JVMa kodu klasy. Można go ściągać, odszyfrowywać albo generować w locie. Umożliwiają instrumentalizację kodu (debugger). Klasy załadowane przez sąsiednie albo potomne Class Loadery są wzajemnie niewidoczne (bezpieczeństwo). Pozwala na załadowanie kilkukrotnie różnego kodu pod tą samą nazwą klasy ale nie wiem czemu ktoś miałby to robić.
Co robią
Dostarczają JVMowi bajty klasy w ściśle zdefiniowanej postaci. Same nie tworzą obiektu Class (te javowe). Robi to natywny kod który sprawdza poprawność i kompatybilność binarną.
Linkowanie
Referencje do obiektów są określane w czasie wykonania a referencje do klas w czasie linkowania. Linkowanie może być robione wcześnie (tuż po kompilacji) albo późno (w czasie wykonania). Java linkuje tak późno jak to możliwe. Typ jest określany przez kwalifikowaną nazwę klasy oraz CL który ją załadował.
Linkowanie jest robione przy wykorzystaniu Class.forName
nie jest robione przy loadClass z Class Loadera – jest przeciążona metoda z flagą loadClass(String name, boolean resolve) ale jest protected.
Klasa do załadowania swoich zależności używa CL który ją załadował.
Inicjalizacja
Klasa która jest zlinkowana ma w Constant Pool mapowania kwalifikowanych nazw klas na ich definicję (Class). Definicja klasy niekoniecznie musi być zainicjalizowana. Tzn. jej statyczne bloki inicjalizujące mogą nie być wykonane tak samo jak przypisania do statycznych pól. Jeśli w wyniku wykonania kodu JVM musi linkować kod to także go inicjalizuje.
Użycie stałej NazwaKlasy.class wykorzystuje metodę loadClass Class Loadera która nie inicjalizuje. Analogiczny kod w kotlinie inicjalizuje companion object. Przeciążone Class.forName(String name, boolean initialize, ClassLoader loader) przy przekazaniu false nie linkuje
Kompatybilność binarna
Przy kompilowaniu klas/modułów oddzielnie klasa która wykorzystuje inną klasę mogła być kompilowana z inną definicją swojej zależności niż ta aktualnie dostępna na Class Pathu. Nie wszystkie zmiany spowodują błąd. Niektóre kompatybilne zmiany [JLS]:
- Reimplementing existing methods, constructors, and initializers to improve performance.
- Changing methods or constructors to return values on inputs for which they previously either threw exceptions that normally should not occur or failed by going into an infinite loop or causing a deadlock.
- Adding new fields, methods, or constructors to an existing class or interface.
- Deleting
private
fields, methods, or constructors of a class. - When an entire package is updated, deleting package access fields, methods, or constructors of classes and interfaces in the package.
- Reordering the fields, methods, or constructors in an existing type declaration.
- Moving a method upward in the class hierarchy.
- Reordering the list of direct superinterfaces of a class or interface.
- Inserting new class or interface types in the type hierarchy.
Podobna sytuacja występuje dla serializacji.
Implementacja Class Loaderów
Obiekt Class powstaje w wyniku wywołania jednej z wersji natywnej metody defineClass w ClassLoader. Class ma referencje do Class Loadera który ją załadował. Dlatego ten sam kod klasy załadowany przez dwa różne ClassLoadery skutkuje dwoma typami (między którymi nie można rzutować).
Natywne metody ładujące są prywatne i są wywoływane przez protected defineClass które robi walidacje certyfikatów o ile są i nazwy ładowanej klasy (np czy pakiet nie zaczyna się od java żeby nie można było załadować swojej wersji Stringa). Bez Security Managera prywatne metody można zawołać refleksją. Przewidziany przez twórców sposób to rozszerzenie klasy ClassLoader i wywołanie metod z walidacją.
Hierarchia Class Loaderów
Klasa Class Loader zawiera metodę szablonową loadClass(String name, boolean resolve) która zakłada locki w przypadku ładowania wielowątkowego (tylko jeden wątek ładuje klasę z wykorzystaniem jednego CL) oraz zapewnia domyślną hierarchiczność CL. Metoda do zaimplementowania to findClass(String name) która musi zwrócić klasę więc w implementacji zawołać którąś z natywnych defineClass. Domyślna hierarchia to :
- Bootstrap
- Extension
- Application/System
Loader najpierw deleguje do rodzica zanim spróbuje załadować sam. Zapewnia to że klasy core’owe będą ładowane tylko raz i to przez Bootstrapowy CL oraz że wszystkie klasy odnosząc się do Stringa dostaną tą samą klasę Stringa.
Bootstrap
Bootstrapowy CL jest napisany w C i nie można mieć referencji do niego z poziomu Javy. Ładuje klasy z JRE. Powtarza część walidacji robionych w javie i dodaje swoje (dt. spójności typów [Bracha])
Extension
Extension CL powstał na potrzeby Apletów (jak być może cały mechanizm CL) i daje alternatywny/globalny „Class Path” czyli miejsce z którego są ładowane klasy dla wszystkich JVMów na danej maszynie o ile nie zmieniały parametrów uruchomieniowych (java.ext.dirs które powinno zawierać foldery a nie pliki).
Extension CL ładuje pliki z pominięciem walidacji Security Managera (? AccessController.doPrivileged() i nie sprawdza uprawnienia createClassLoader).
System
Systemowy CL ładuje kod z Class Patha.
Problemy
Nie zawsze CL działają parent first, servlety są child first:
„The Web application class loader must load classes from the WEB-INF/ classes directory first, and then from library JARsB in the WEB-INF/lib directory.” str 109 spec 3.0
To może powodować problem jeśli klasa możliwa do załadowania przez rodzica i potomnego. Kod rodzica będzie używał innej klasy niż kod potomny i nie będą mogły przekazywać instancji tej klasy w wywołaniach między sobą.
Przy ładowaniu parent first problem pojawia się jeśli kod core’owy (rodzica) korzysta z klas na Class Path np. Xerces (xml), JNDI (service locator).
Przy każdym sposobie ładowania pojawia się problem współpracy między kodem załadowanym przez różne CL. Musi się to odbywać przez interfejs który jest wspólny dla obu (ładowany prawdopodobnie przez trzeciego CL do którego oba mają referencje).
ContextClassLoadery
Żeby zignorować hierarchie wprowadzono ContextClassLoadery. To pole w Thread do którego można wsadzić Class Loadera (żeby nie trzeba było do Thread Locala). A później w tym samym wątku ale w klasie załadowanej przez innego CL zignorować jej aktualnego CL i użyć tego z wątku.
Źródła:
„The Java® Virtual Machine Specification” Tim Lindholm, Frank Yellin, Gilad Bracha, Alex Buckley
„Inside the Java Virtual Machine” Bill Venners
Dynamic Class Loading in the Java VirtualMachine Sheng Liang, Gilad Bracha
Dokumentacja mechanizmu rozszerzeń
Rożnica między Class.forName() i ClassLoader.loadClass()