Java Class Loaders: Deep-Dive Reference Guide
1. What is a Java Class Loader?
A Java ClassLoader is a part of the Java Runtime Environment (JRE) responsible for dynamically loading classes into the Java Virtual Machine (JVM) during runtime.
Java applications do not need to know the physical file location of classes in advance. The classloader handles locating, loading, and linking class bytecode as needed.
Key Characteristics:
- Classes are loaded only when first referenced.
- Each class is loaded by a specific class loader instance.
- Classes loaded by different class loaders are considered different even if they are identical bytecode.
2. How Java ClassLoader Works
When a class is needed, JVM follows this flow:
- Delegation Model: Delegates class loading request to its parent before attempting to load itself.
- Namespace Isolation: Every ClassLoader has a separate namespace.
- Linking & Verification: Once a class is loaded, it's verified and linked.
Class<?> myClass = Class.forName("com.example.MyClass");
Here, Class.forName
internally uses the current thread’s context class loader to locate and load the class.
When a class is referenced, the JVM asks the classloader to find it. The classloader:
- Locates the .class file.
- Loads it into memory.
- Verifies its structure and bytecode.
- Prepares memory for static fields.
- Resolves references to other classes.
Each loaded class is associated with the classloader that loaded it. Multiple versions of the same class can coexist in the JVM if loaded by different classloaders.
Delegation Model:
When a class loader receives a request to load a class, it first delegates the request to its parent. If the parent can't load it, only then the current loader attempts.
📌 Output Insight: String
is part of the core JDK, so even the App ClassLoader delegates it to the Bootstrap ClassLoader (hence null
).
Namespace Isolation:
Each class loader maintains its own set of loaded classes. Same class name loaded by different loaders are considered different.
📌 Real-world Use: This is how application servers isolate applications (WARs) using different class loaders.
Linking & Verification
After loading, the JVM links the class — verification (for bytecode safety), preparation (static fields), and resolution (symbolic -> direct references).
📌 Note: When Class.forName()
is called, the JVM loads and links VerificationExample
. The static block runs during class initialization.
3. Types of Class Loaders (Traditional)
3.1. Bootstrap ClassLoader (Primordial)
- Loads core Java classes (e.g., java.lang.*, java.util.*).
- Implemented in native code.
- Loads from JAVA_HOME/lib.
3.2. Extension (Platform) ClassLoader
- Loads classes from JAVA_HOME/lib/ext or the modules path.
- Also known as PlatformClassLoader from Java 9 onwards.
3.3. Application (System) ClassLoader
- Loads classes from the classpath (e.g., -cp, CLASSPATH env var).
- Default loader for user applications.
3.4. Custom ClassLoaders
- Subclasses of java.lang.ClassLoader.
- Used for module isolation, reloading classes, secure loading.
ClassLoader myLoader = new MyCustomClassLoader();
4. Modern ClassLoader Enhancements
Java 9+ Modular System
- AppClassLoader, PlatformClassLoader, and BuiltinClassLoader under jdk.internal.loader.
- Strong encapsulation in Jigsaw (Module System).
URLClassLoader
- Loads classes and resources from file system or remote URLs.
Thread Context ClassLoader
- Often used in frameworks (e.g., JNDI, JAXB, JDBC)
5. ClassLoader Hierarchy
Bootstrap ClassLoader
↓
Platform/Extension ClassLoader
↓
Application ClassLoader
↓
(Custom or Container ClassLoaders)
Example:
If a class com.abc.MyClass
is present in both /app/lib/
and JAVA_HOME/lib/ext
, it will be loaded by the PlatformClassLoader.
6. Thumb Rules in Class Loading
- Parent-first delegation: All standard loaders follow this.
- Child-first for web apps (e.g., Tomcat/Payara WAR loaders).
- Duplicate class versions must be isolated by class loaders.
- JAR conflict leads to ClassCastException or LinkageError.
- Use -verbose:class or -Xlog:class+load=info to trace loading.
7. Application Server ClassLoaders (Payara / GlassFish)
Types:
- Bootstrap: JVM internal classes.
- OSGi Module ClassLoader: Payara modules.
- Domain ClassLoader: /domains/domain1/lib
- WebApp ClassLoader: /applications/<app>/WEB-INF/lib
WebApp ClassLoader Behavior:
- delegate="true" (default): Tries parent loaders first. Server classes override application classes.
- delegate="false": Tries WAR classes first, then domain/lib, then modules. Application JARs override server classes.
- Classes loaded from WEB-INF/lib are unique to the app.
- If a class is available in domain1/lib or /modules, it is shared.
- Class conflicts often occur due to overlapping libraries.
- What it is: This is the OSGi ClassLoader used internally by Payara to load core services, admin modules, and libraries from the /modules and /modules/autostart directories.
- Also includes: Other internal framework components like Hazelcast, HK2, Jersey, Jackson (default versions), and any bundled JARs marked as OSGi bundles.
- Used to load: Payara services (transaction manager, resource adapters, monitoring, etc.). Admin CLI and console plugins. Any JARs Payara bundles as default runtime
- ⚠️ Important note: Classes loaded here are not visible to application code unless explicitly exported via OSGi metadata.
- What it is: This is the Web Application ClassLoader, used to load application-specific classes and libraries.
- Used to load: Your app’s .class files. Libraries in WEB-INF/lib/ (including your version of libraries like nimbus-jose-jwt, bcprov, jackson, etc.)
- Parent delegation: Delegates to Domain ClassLoader → which delegates to OSGi ClassLoader. Respects delegate="true" or delegate="false" rules (configured in glassfish-web.xml)
Example:
<class-loader delegate="false"/>
In this case, even if hk2-api.jar is present in /modules, it will be loaded from WEB-INF/lib if found.
8. Real-World Debugging Case: Nimbus JOSE + BouncyCastle
🔍 Problem
- App failed with ClassNotFoundException: org.bouncycastle.util.encoders.Base64.
- Nimbus JAR was in domain1/lib.
- BouncyCastle JARs (bcprov, bcpkix) were also in domain1/lib.
- <class-loader delegate="false"/> was active.
🧠 Root Cause
Nimbus loaded via domain classloader, but BouncyCastle was loaded via WEB-INF/lib. Due to different classloaders, Nimbus could not access BouncyCastle.
✅ Resolution
Moved nimbus-jose-jwt-10.0.1.jar
to WEB-INF/lib
— so both libraries shared the same WebApp classloader, resolving the error.
9. ClassLoader Delegation Model
The JVM follows a parent-first delegation model by default:
AppClassLoader -> ExtClassLoader -> BootstrapClassLoader
However, application servers like Payara allow configuring child-first via:
<class-loader delegate="false"/>
This prioritizes WEB-INF/lib before domain/lib and modules.
🔍 Summary:
- Each classloader delegates upward to its parent.
- Parents cannot access children’s classes (i.e., OSGi cannot see Domain, Domain cannot see WebApp).
- If delegate="true", WebApp CL first checks Domain → OSGi → System → Bootstrap.
- If delegate="false", WebApp CL tries to load the class itself before asking parents.
- If your JAR tries to reference another class not in its own scope (e.g., Nimbus referencing BouncyCastle in domain1/lib), and visibility is broken, you'll get ClassNotFoundException or NoClassDefFoundError.
- May lead to duplication if classes are also loaded by parent classloaders.
- Must ensure dependencies (like BouncyCastle) are placed where they’re visible to the same loader or higher up in the hierarchy.
- Some OSGi-enabled JARs (e.g., nimbus-jose-jwt, jersey-*, jackson-*) are registered and activated by Payara at server startup.
- These become active bundles managed by the OSGi ClassLoader.
- Even with delegate="false", if your application references a class that’s already loaded and cached by an OSGi bundle, your version in WEB-INF/lib might get ignored or conflict at runtime.
Class Visibility Still Respects ClassLoader Hierarchy
- Let's say your app uses Nimbus (from WEB-INF/lib), and Nimbus internally calls Class.forName("org.bouncycastle...").
- If bcprov is not in WEB-INF/lib, but only in /domain1/lib, the child classloader (WebApp CL) can delegate up to Domain CL — but not downward.
- If Nimbus was mistakenly loaded by OSGi CL (because of duplication), it cannot see bcprov in your WAR → leading to ClassNotFoundException.
OSGi Enforces Strict Bundle Isolation
- OSGi bundles have explicit Import-Package and Export-Package constraints.
- Even with child-first loading, your WAR classloader cannot override what OSGi bundles expose internally, unless:
- You completely isolate the JARs (e.g., move all to WEB-INF/lib),
- Or avoid duplicating sensitive JARs like Jersey, Jackson, Nimbus, etc.
- JARs in /domain1/lib are loaded by the Domain ClassLoader.
- JARs in /modules are loaded by the OSGi ClassLoader.
- If a class exists in both /domain1/lib and /modules, and Payara has already loaded it from modules (via OSGi), it will not load the one from domain1/lib — even though delegate="false".
- Note : Payara pre-wires and initializes OSGi services before web apps are loaded. So those classes are already loaded by the time your app starts.
OSGi is a dynamic module system for Java. It allows applications to be composed of many smaller components (called bundles) that can be installed, started, stopped, updated, and uninstalled without requiring a reboot.
- In traditional Java applications:
- All classes exist in a flat classpath.
- No modularity, version isolation, or runtime dynamism.
- Enabling true modularity (split code into independently deployable bundles).
- Enforcing explicit dependencies (via manifest).
- Allowing hot updates of individual components.
- Providing isolation and controlled class visibility between modules.
- This bundle is named com.example.payment.
- It exports its api package.
- It imports packages it depends on (not full JARs!).
OSGi uses per-bundle class loaders, unlike Java EE’s global or WAR-level class loaders.
Key rules:
- Each bundle has its own classloader.
- A bundle can only access: Its own classes. Classes from packages it explicitly imports.
- Even if two bundles contain the same class, they're different types if loaded separately.
- Payara modules under /modules follow an OSGi-style packaging:
- JARs include OSGi metadata in MANIFEST.MF.
- The server uses a HK2 + OSGi hybrid model to isolate internal components.
- Example: hk2-locator.jar, jersey-server.jar — all export/import packages rather than relying on flat classpath
Scenario:
- You place nimbus-jose-jwt.jar in /domain1/lib, but it’s already in /modules as an OSGi bundle. Now:
- If your WAR uses <class-loader delegate="false"/>, it skips module loader.
- OSGi's bundle loader doesn't export internal packages to domain/lib or web apps.
- Hence, you must include it in WEB-INF/lib to make it available within your app’s classloader scope.
- OSGi enables versioned, isolated, hot-swappable Java modules.
- It solves complex classpath issues in large applications.
- It’s widely used in Java EE servers like Payara, Eclipse IDE, and IoT devices.
- Requires rethinking application structure — avoid flat lib/, use package-level import/export.
- Jersey (REST framework)
- Jackson (JSON processing)
- HK2 (dependency injection)
- Jakarta EE APIs (Servlet, JAX-RS, JAXB, etc.)
- BouncyCastle (for crypto)
- Others like Grizzly, Weld, Mojarra (JSF), etc.
- Debug classloading issues : Helps confirm whether a particular component (e.g., jersey-server, hk2-locator, etc.) is available via Payara's module system.
- Avoid duplication: Before placing a JAR in domain1/lib or WEB-INF/lib, check if it's already loaded as a module.
- Verify OSGi version: Some modules may be versioned — this command shows exact versions being loaded.
- Confirm availability of internal APIs : Especially useful when <class-loader delegate="true"/> is used and you're relying on server-provided libraries.
- Loop through all JARs: It iterates over each *.jar file in the current directory.
- Read Manifest: For each JAR, it extracts the META-INF/MANIFEST.MF using unzip -p (without extracting to disk).
- Search for OSGi identifier: It greps for the Bundle-SymbolicName: header, which is a mandatory entry for an OSGi bundle and uniquely identifies the bundle in the OSGi runtime.
- Print OSGi JARs: If found, it prints the JAR filename along with its symbolic name in a formatted way.
- Sort output: The output is then sorted alphabetically.
Appendix: ClassLoader Debug Commands
Run with JVM option: -verbose:class
Example output:
[30.849s][info][class,load] org.glassfish.hk2.extension.ServiceLocatorGenerator source: file:/opt/payara6/glassfish/domains/domain1/applications/auruspay/WEB-INF/lib/hk2-api-3.1.1.jar
# Show what class loader loaded which class
java -Xlog:class+load=info -cp yourapp.jar com.example.MainClass
# In Payara:
export _JAVA_OPTIONS='-Xlog:class+load=info'
<jvm-options>-Xlog:class+load:file=${com.sun.aas.instanceRoot}/logs/loader.log</jvm-options>
This document consolidates class loader internals with hands-on troubleshooting seen in Payara deployments.