CHF -> Class loaders

Class loaders in Java

Class loaders are responsible for finding and loading Java classes into the Java Virtual Machine (JVM) at runtime.

What is the mechanism of classloading?

In the Classloader hierarchy, when a class is requested, the request is delegated from the top (Bootstrap Classloader) to the bottom (User-defined Classloaders) until the class is found or an exception is thrown. If a Classloader can’t find the requested class, it delegates the task to its parent Classloader. This delegation model helps maintain class uniqueness, security, and separation of concerns in the Java runtime environment.

Class Loading Process consists of three main steps: loading, linking, and initialization.

  • loading: The classloader reads the binary data of the class from the source (e.g., .class file or JAR file) and creates a java.lang.Class object in the JVM,
  • linking: the loaded class is verified (if it’s structurally correct and up to JVM specifications), static fields are prepared (memory allocation and initialization with default values), and symbolic references resolved (symbolic references in the class are replaced with direct references to other classes or methods).
  • initialization: the class’s static fields are initialized with their proper values, and static initializer blocks are executed in the order they appear in the source code.

Default hierarchy of classloaders.

The classloaders hierarchy consists of the following: 

  • Bootstrap Classloader, 
  • Extensions Classloader, 
  • System (or Application) Classloader, 
  • User-Defined Classloaders.

What is the Delegation Model?

Delegation Model:

  • ensures that classes are loaded by the appropriate classloader in the hierarchy,
  • promotes class separation and security, 
  • classloader first delegates the request (to load a class) to its parent classloader until the request reaches the Bootstrap classloader. Then, Bootstrap classloader attempts to load the class. If succeeds, it returns the loaded class. Otherwise, the request is passed to the next classloader in the hierarchy. If no classloader can load the requested class, a ClassNotFoundException is thrown.

What is the role of every element in hierarchy?

Bootstrap Classloader:

  • also known as the primordial classloader
  • parent of all classloaders in Java, 
  • part of the core JVM and is implemented natively, 
  • responsible for loading the core Java libraries (like java.lang, java.util, etc.) from the <JAVA_HOME>/lib directory
  • ensures the basic Java classes are loaded before any other classloaders are invoked.

Extensions Classloader:

  • also called the Platform Classloader
  • child of the Bootstrap classloader,
  • loads classes from <JAVA_HOME>/lib/ext directory 
  • provides a way to extend the functionality of the core Java libraries without modifying the core classes themselves.

System Classloader:

  • also known as the Application classloader,
  • child of the Extensions classloader, 
  • loads classes from the classpath, which is defined by the CLASSPATH environment variable, the -cp or -classpath command-line options, or the Class-Path attribute in the JAR manifest file,
  • responsible for loading application classes and third-party libraries.

User-Defined Classloaders:

  • also known as User-Defined Classloaders,
  • Developers can create custom Classloaders by extending the java.lang.ClassLoader class, 
  • useful in case modification or extension of the default class-loading mechanism is needed,
  • f.e. can load classes from a remote server, from a database, or with specific access controls.

In the Classloader hierarchy, when a class is requested, the request is delegated from the top (Bootstrap Classloader) to the bottom (User-defined Classloaders) until the class is found or an exception is thrown. If a Classloader can’t find the requested class, it delegates the task to its parent Classloader. This delegation model helps maintain class uniqueness, security, and separation of concerns in the Java runtime environment.

Why don’t we use only one or two classloaders?

Using only one or two classloaders in Java might seem like a simpler approach, but there are several reasons why the multiple classloader hierarchy is better:

  • Separation of concerns: The classloader hierarchy ensures that core Java libraries, extension libraries, application classes, and user-defined classes are loaded and managed by separate classloaders. This separation of concerns makes it easier to maintain and troubleshoot issues related to class loading, as each classloader is responsible for a specific set of classes.
  • Security: The hierarchy of classloaders helps maintain security in the JVM. Classes loaded by the Bootstrap classloader have access to core Java APIs and can perform sensitive operations. By having a separate classloader for application classes, it prevents them from accidentally or maliciously modifying core Java classes. This separation also allows security policies to be enforced at different levels, providing additional protection.
  • Class isolation and versioning: Multiple classloaders allow for creating isolated environments for different parts of an application or for multiple applications running in the same JVM. This isolation prevents class name conflicts or versioning issues that can arise when different parts of an application or multiple applications use different versions of the same library.
  • Customization and flexibility: With a hierarchy of classloaders, developers can create custom classloaders by extending the java.lang.ClassLoader class. These custom classloaders can be used to modify or extend the default class-loading mechanism, allowing for greater flexibility in handling application-specific requirements.
  • Caching and performance: classloaders cache the classes they load to improve class loading performance. When a class is requested, the delegation model ensures that each classloader first checks its cache before delegating the request to its parent. This reduces the need to load the same class multiple times and can improve the overall performance of an application.

Life cycle of a ‘class’ when it is loaded by classloaders.

The lifecycle of a class in Java, when it is loaded by class loaders, can be summarized in the following stages: 

  1. Loading: The first step in the lifecycle of a class is loading. When a class is requested, the classloader reads the binary data from the source (e.g., .class file, JAR file, or a custom source) and creates a java.lang.Class object in the JVM. The classloader first delegates the request to its parent in the hierarchy, and each class loader attempts to load the class starting from the Bootstrap classloader down to the custom classloaders. If the class is found, the loading process is completed, and the binary data is used to create a java.lang.Class object.
  2. Linking: Once a class is loaded, it goes through the linking process, which consists of three sub-stages:
    1. Verification: During this stage, the JVM verifies that the loaded class is structurally correct and adheres to the JVM specifications. This ensures that the class doesn’t contain any malformed code that might compromise the JVM’s integrity or security.
    2. Preparation: During this stage, memory is allocated for the class’s static fields, and they are initialized with default values.
    3. Resolution: In this stage, the JVM replaces the symbolic references in the loaded class with direct references to other classes, methods, or fields. This process resolves the links between the class being loaded and other classes that it depends on.
  3. Initialization: During the initialization stage, the class’s static fields are assigned their proper values, and the static initializer blocks (if any) are executed in the order they appear in the source code. This step ensures that the class is fully initialized and ready for use.
  4. Usage: Once the class is loaded, linked, and initialized, it can be used by the application. This includes creating instances of the class, invoking its methods, and accessing its fields.
  5. Unloading: When no longer needed (e.g., the application has completed or the classloader that loaded the class is no longer reachable), the JVM may unload the class to reclaim memory. The garbage collector identifies and collects any unused class objects and their associated data. It is important to note that class unloading depends on the JVM implementation and its garbage collection algorithms. Some JVM implementations may not unload classes at all, while others may unload them under specific circumstances.

Stages of class construction.

In Java, the construction of a class, particularly the instantiation of an object, involves several stages. Here is a breakdown of the stages involved in class construction:

  1. Declaration – The first stage in class construction is declaring the class. This involves specifying the class name, any access modifiers (e.g., public, private, or protected), and optionally extending a superclass or implementing interfaces.
  2. Definition (Implementation) – During this stage, you define the class’s properties and methods.
  3. Compilation – Once you have declared and defined the class, you must compile the Java source code into bytecode.
  4. Object Instantiation
    1.  Memory Allocation
    2. Initialization
  5. Constructor Invocation 

User-defined classloader

User-Defined Classloaders:

  • also known as User-Defined Classloaders,
  • Developers can create custom Classloaders by extending the java.lang.ClassLoader class, 
  • useful in case modification or extension of the default class-loading mechanism is needed,
  • f.e. can load classes from a remote server, from a database, or with specific access controls.

Common Use Cases for Custom Class Loaders:

  • Loading classes from non-standard locations: Custom classloaders can be used to load classes from locations other than the file system, such as a remote server, a database, or a different directory structure.
  • Reloading classes at runtime: By implementing a custom classloader, you can reload classes without restarting the JVM, which is useful in development environments or application servers where frequent code updates are needed.
  • Isolating class loading: Custom classloaders can be used to create isolated environments for different parts of an application, preventing class name conflicts or versioning issues.
  • Enforcing security policies: User-defined classloaders can be used to apply specific security policies, such as controlling access to sensitive resources or enforcing code signing.

Library Version Conflicts and how classloaders resolve it.

Library version conflicts can occur when multiple libraries or applications use different versions of the same library or dependency. This can lead to errors and unpredictable behavior, as the different versions of the library may have conflicting dependencies or functionality.

In Java, classloaders are responsible for loading classes and resources at runtime, and they play a crucial role in resolving library version conflicts. Classloaders typically follow a delegation model.

This delegation model is used to resolve library version conflicts in the following way:

  • When an application requests a class or resource, the classloader first searches its own classpath for the class or resource.
  • If the class or resource is not found in its own classpath, the classloader delegates the request to its parent classloader.
  • The parent classloader then searches its own classpath for the class or resource.
  • If the class or resource is found in the parent classloader’s classpath, it is loaded and returned to the child classloader.
  • If the class or resource is not found in the parent classloader’s classpath, the parent classloader delegates the request to its own parent classloader, and so on, until the root classloader is reached.
  • If the class or resource is not found by any of the classloaders in the hierarchy, a ClassNotFoundException or NoClassDefFoundError is thrown. 

By using this delegation model, classloaders can ensure that each class is loaded only once and that conflicts between different versions of the same library are avoided. However, if multiple versions of a library are required by different parts of an application, a more complex solution, such as using multiple classloaders or creating a custom classloader, may be necessary to manage the conflicts.

Is it possible (and how) to load different versions of the same libs.

In some cases, it may be necessary to load different versions of the same library or dependency in a single application.

One way to load different versions of the same library is to use a custom classloader that can load classes and resources from specific locations, such as separate JAR files or directories. This allows you to create a separate classpath for each version of the library and load them independently.

By creating an isolated classloader, you can ensure that the classes and resources loaded by the isolated classloader are independent of the system classloader and other classloaders. This can be useful in certain situations, such as when you need to load classes with different versions or when you want to ensure that the classes and resources loaded by your application are not affected by other applications or libraries. 

Any user-created classloader becomes a child classloader of the system classloader. It can access all the classes and resources loaded by its parent classloader, but not vice versa.

What Types of Exceptions are thrown by the classloading mechanism?

Common exceptions and errors thrown by the Java classloading mechanism:

ClassNotFoundException: when JVM cannot locate the class definition file (.class) that it is trying to load.

NoClassDefFoundError: when JVM cannot find the class definition.

ClassCastException: when attempting to cast an object of one class to another class that is not compatible.

UnsupportedClassVersionError: when JVM tries to load a class compiled with a newer version of Java than the JVM supports.

IllegalAccessException: when an application tries to access a forbidden class, field, method, or constructor (due to visibility modifiers).

InstantiationException: when JVM cannot create an instance of a class, typically because the class is abstract or an interface, or does not have a public, no-argument constructor.

LinkageError: when there is an issue with linking a class or interface during runtime, such as a missing, changed, or incompatible class or method.

These are some of the most common exceptions thrown by the classloading mechanism in Java. It is important to handle these exceptions appropriately in your code to ensure that your application can continue to function properly, or at least fail gracefully, when encountering such issues.

Leave a comment