Sunday, May 4, 2025

Java ClassLoader Deep-Dive and Real-World Behavior in Payara

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. 
Payara Server ClassLoader (aka Server ClassLoader)
  • 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. 
Application ClassLoader (Web or EAR ClassLoader)
  • 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.
 
⚠️ Risks / Considerations:
  • 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.
 
OSGi-Bundled JARs (in /modules) may load themselves first — regardless of delegation
  • 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. 
OSGi & Classloader Conflicts in Payara
  • 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.
 
 
🔷 What is OSGi?
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. 
 
🔷 Why OSGi?
  • In traditional Java applications:
  • All classes exist in a flat classpath.
  • No modularity, version isolation, or runtime dynamism.
OSGi solves this by:
  • 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. 
🔷 Core Concepts 

 
🔷 Bundle Example 

MANIFEST.MF inside an OSGi bundle:
 
This declares:
  • This bundle is named com.example.payment.
  • It exports its api package.
  • It imports packages it depends on (not full JARs!). 
🔷 OSGi Class Loading Hierarchy
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. 
  
🔷 How OSGi is Used in Payara / GlassFish
  • 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
 
🔷 Real-World Scenario: Why ClassNotFoundException Happens with OSGi
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 in Practice: Tools
 
 
 
🔷 Summary
  • 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. 
The ./asadmin list-modules command is used in Payara Server (or GlassFish) to list all the OSGi modules that are currently registered and available in the server’s module system.
 
You get a list of installed HK2/OSGi modules, which are essentially internal system components and libraries that are managed as modules by the server. These modules are typically loaded from the server’s /modules directory and include:
  • 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. 
 
📍 Use Cases
  • 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.
 
 
 
Below command ./asadmin list-modules | grep OSGiModuleImpl:: | awk -F= '{print $2}' | cut -d[ -f2 | cut -d] -f1 | sort is used to extract and list the versions of all active OSGi modules deployed in a Payara (or GlassFish) server. It starts by invoking asadmin list-modules to retrieve all registered modules, filters only those associated with OSGi using grep OSGiModuleImpl::, then uses awk and cut to isolate the version number from the module's display name (which is typically formatted like OSGiModuleImpl::module-name [version]), and finally sorts the output. This command is useful for auditing the versions of OSGi modules actively loaded by the server, helping developers avoid conflicts with application-level JARs and diagnose class-loading issues. 

 
 
Below command cat /opt/payara6/glassfish/domains/domain1/osgi-cache/felix/bundle*/bundle.info | grep "file:" is used to extract the physical file paths of all OSGi bundle JARs currently cached and loaded by the Felix OSGi container within a Payara domain. By reading all bundle.info files inside each bundle* directory of the osgi-cache, it lists entries containing the file: prefix, which indicates the actual location of the bundle JAR on disk. This is helpful for identifying which exact versions of JARs were resolved and activated by the OSGi framework, and can be used to troubleshoot classpath conflicts, verify module loading behavior, or audit the internal module state of the server. 
 
 
 
This script scans all JAR files in the current directory and checks if they are OSGi-enabled bundles by looking for the Bundle-SymbolicName header in their MANIFEST.MF files. Here's what it does step-by-step:

  • 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.