Gain flexibility in assembling and extending your application with the Java class loader architecture.
Introduction and Motivation
A practitioner's understanding of the Java platform cannot be considered complete without a solid understanding of the life cycle of a Java reference type (class or interface). The primary processes involved in this life cycle include loading, linking, initialization, and unloading. This paper discusses one of these processes, namely the loading process, in some detail.
Class loading is fundamental to two of the most compelling features of Java: dynamic linking and dynamic extension.
Dynamic linking allows types to be incrementally incorporated into a running JVM. Dynamic extension allows the decision as to which types are loaded into a JVM to be deferred until runtime. This means that an application hosted by the JVM can leverage types that were previously unknown, or perhaps did not even exist, when the application was compiled. Dynamic linking and extension support the notion of runtime assembly of code, a notion of critical importance to server-side containers (e.g. application servers).
A Note on Terminology
The terms "loading" and "class loading" in this article refer to the loading of reference types (classes, interfaces, and arrays). Where "class" or "type" is used, all reference types are usually implied. Explicit language will be used when this is not the case.
Type Life Cycle
Types are made available to a running program by the Java Virtual Machine. This is done through a process of loading, linking, and initialization. Loading is the process of locating the binary representation of a type and bringing it into the JVM. Linking is the process of taking the type and incorporating it into the runtime state of the JVM so that it can be executed. Initialization is the process of executing the initializers of a type (static initializers for classes; static field initializers for classes and interfaces). When a type becomes unreachable (i.e. the running application has no references to the type), it becomes eligible for garbage collection. This collection is called type unloading.
Loading a type, from a more detailed perspective, consists of three primary activities:
- Given a type's fully qualified name, produce a stream of binary data that represents that type
- Parse the stream of binary data produced in step #1 into internal structures in the method area of the JVM. The method area is a logical area of memory in the VM that is used to store information about loaded types.
- Create an instance of class
java.lang.Class that represents the type indicated in step #1
Further details about linking, initialization, and unloading are beyond the scope of this article. Interested readers are referred to the JVM Specification for more information.
When is a Type Loaded?
This is a surprisingly tricky question to answer. This is due in large part to the significant flexibility afforded, by the JVM spec, to JVM implementations.
Loading must be performed before linking and linking must be performed before initialization. The VM spec does stipulate the timing of initialization. It strictly requires that a type be initialized on its first active use (see Appendix A for a list of what constitutes an "active use"). This means that loading (and linking) of a type MUST be performed at or before that type's first active use.
Implementations typically delay the loading of a type as long as possible. They could potentially, however, load classes much earlier. Class loaders (see below) can opt to load a type early in anticipation of eventual use. If this strategy is chosen, the class loader must not report any problem (by throwing a subclass of
java.lang.LinkageError) encountered during loading until the type's first active use. In other words, a type must appear to be loaded only when needed.
Classes are loaded so that their bytecodes can be processed by the execution engine of the JVM. All non-array (see Appendix B) reference types, including classes, are loaded either through the bootstrap class loader - sometimes referred to as the primordial class loader - or through a user-defined class loader - sometimes referred to as a custom class loader. The bootstrap class loader is an integral part of the JVM and is responsible for loading trusted classes (e.g. basic Java class library classes). User-defined class loaders, unlike the bootstrap class loader, are not intrinsic components of the JVM. They are subclasses of the
java.util.ClassLoader class that are compiled and instantiated just like any other Java class.
Different user-defined class loaders can be specialized to provide different loading policies. For example, a class loader might cache binary representations of classes and interfaces, prefetch them based on expected usage, or load a group of related classes together (as mentioned before, however, a class loader must reflect loading errors only at points in the program where they could have arisen without prefetching or group loading).
Class loaders, in addition to loading classes, can also be used to load other types of resources (e.g. localization resource bundles) from the repositories that they manage. Further treatment of this capability is outside of the scope of this article.
A delegation model for class loaders was introduced in Java 2 (starting with version 1.2). In this model, class loaders are arranged hierarchically in a tree, with the bootstrap class loader as the root of the tree. Each user-defined class loader is assigned a "parent" class loader when it is constructed (the bootstrap class loader is the only class loader in the system without a parent). By default, this parent is the system class loader (see below). Alternately, a parent class loader can be provided explicitly as a construction parameter.
When a load request is made of a user-defined class loader, that class loader can either immediately attempt to load the class itself, or it can first delegate to some other class loader - only attempting to load the class itself if the delegate fails to do so. A class loader that has been delegated, provided it is not the bootstrap class loader, has the same choice - attempt to load the class itself or delegate to yet another class loader. If a class loader is eventually successful at loading the type, it will be marked as the defining class loader for the type. All of the class loaders that were given an opportunity to load the type are marked as initiating loaders of the type (see Appendix C). If no class loader is successful in loading the type, a
java.lang.ClassNotFoundException (or, in certain cases, a
java.lang.NoClassDefFoundError) will be thrown.
A user-defined class loader is usually implemented such that load attempts are delegated to the class loader's parent before the class loader attempts to load the class itself. This, however, is not strictly required. The class loaders used to load servlets in a servlet engine (e.g. Tomcat), for example, are developed in such a way that they attempt to load a class directly before delegating the request (see SRV.9.7.2 of the "Java Servlet Specification", v2.3).
Typical Default Class Loader Hierarchy
By default, a Java 2 JVM typically provides a bootstrap class loader and two user- defined class loaders: the extension class loader and the system (or application) class loader. The bootstrap class loader, as mentioned above, is responsible for loading the core Java classes (e.g.
javax.*, etc.) into the VM. The extension class loader is responsible for loading classes from the JRE's extension directories. Finally, the system class loader is responsible for loading classes from the system class path.
A loaded class in a JVM is identified by its fully qualified name and its defining class loader - this is sometimes referred to as the runtime identity of the class. Consequently, each class loader in the JVM can be said to define its own namespace. Within a namespace, all fully- qualified names are unique. Two different name spaces, however, can contain identical fully-qualified names. Because the defining class loader is part of the runtime identity of the class, the classes associated with these names are considered distinct (e.g. class
x.y.z.Foo defined by class loader A is NOT considered by the JVM to be the same class as class
x.y.z.Foo defined by class loader B).
Ensuring type safe linkage in the presence of multiple class loaders (some of which are user-defined) requires special care. In fact, prior to Java 2, Java was not type-safe (see "Java is not type-safe" by V.Saraswat). To ensure type safety (i.e. type consistency across namespaces), the JVM now imposes loading constraints. A loading constraint indicates that a name in one namespace MUST refer to the same type data in the method area as the same name in another namespace. The JVM will, at prescribed times, add new constraints. This is generally done when the JVM encounters references to types whose loading was not initiated by the same class loader that initiated loading of the referencing type. When the JVM attempts to resolve a symbolic reference (during the linking phase of the type life cycle), it must first check that all of the current loading constraints are satisfied. If any of the constraints are violated, a
LinkageError will be thrown.
The Java class loader architecture affords a Java developer a tremendous amount of flexibility in the way that an application is assembled and extended. To truly leverage this flexibility, a developer must understand class loading in some detail. This article presented some of the basic concepts and notions in this area. The next article in this series will provide information about customizing the class loading process, including details on developing user-defined class loaders.
"The Java Virtual Machine Specification, Second Edition" by T.Lindholm and F.Yellin
"Inside the Java 2 Virtual Machine" by B.Venners
"Dynamic Class Loading in the Java Virtual Machine" by S.Liang and G.Bracha
Appendix A: Active Uses
The following activities qualify as active uses; all other uses are considered passive uses:
- Creation of a new instance of a class (e.g. via execution of a
new instruction, implicit creation of a
String, reflection, cloning, deserialization}
- Invocation of a class method (i.e. a static method) declared by a class
- Use or assignment of a non-constant static field declared by a class or interface
- Invocation of certain reflective methods in the Java API, such as methods in
java.lang.Class or in classes in the
- Initialization of a subclass of a class (initialization of a class requires prior initialization of its superclass)
- Designation of a class as the initial class (with
main()) when the JVM is started
Appendix B: Loading Array Classes
NOTE: Array classes are created by the Java Virtual Machine rather than by a class loader. The JVM will, however, assign a defining class loader to each array class. An array of reference types will be assigned the defining class loader of the component type (e.g.
Foo would be assigned the defining class loader of
Foo). The JVM will assign the bootstrap class loader as the defining class loader of an array of primitives.
Appendix C: Initiating and Defining Class Loaders
Because of the class loader delegation model, the class loader that initiates the loading of a type is not necessarily the class loader that defines that type. The class loader responsible for defining a type is referred to as that type's defining class loader. Any class loader that initiates the loading of a type, directly or indirectly (due to delegation), is referred to as an initiating class loader of the that type. Note that the defining class loader of a type is also an initiating class loader for that type.
About the Author
Brandon E Taylor is a software architect in the Network Storage division of Sun Microsystem, Inc. He has worked for Sun since 1998 and has been developing "serious" software using Java since 1996. (By the way...the "E" stands for "Eugene")