Sponsored Link •
From the programmer's perspective, one of the most important aspects of Java's architecture to understand is the linking model. As mentioned in earlier chapters, Java's linking model allows you to design user-defined class loaders that extend your application in custom ways at run-time. Through user-defined class loaders, your application can load and dynamically link to classes and interfaces that were unknown or did not even exist when your application was compiled.
The engine that drives Java's linking model is the process of resolution. The previous chapter described all the various stages in the lifetime of a class, but didn't dive into the details of loading and resolution. This chapter looks at loading and resolution in depth, and shows how the process of resolution fits in with dynamic extension. It gives an overview of the linking model, explains constant pool resolution, describes method tables, shows how to write and use class loaders, and gives several examples.
When you compile a Java program, you get a separate class file for each class or interface in your program. Although the individual class files may appear to be independent, they actually harbor symbolic connections to one another and to the class files of the Java API. When you run your program, the Java virtual machine loads your program's classes and interfaces and hooks them together in a process of dynamic linking. As your program runs, the Java virtual machine builds an internal web of interconnected classes and interfaces.
A class file keeps all its symbolic references in one place, the constant pool. Each class file has a constant pool, and each class or interface loaded by the Java virtual machine has an internal version of its constant pool called the runtime constant pool. The runtime constant pool is an implementation-specific data structure that maps to the constant pool in the class file. Thus, after a type is initially loaded, all the symbolic references from the type reside in the type's runtime constant pool.
At some point during the running of a program, if a particular symbolic reference is to be used, it must be resolved. Resolution is the process of finding the entity identified by the symbolic reference and replacing the symbolic reference with a direct reference. Because all symbolic references reside in the constant pool, this process is often called constant pool resolution.
As described in Chapter 6, "The Java Class File," the constant pool is organized as a sequence of items.
Each item has a unique index, much like an array element. A symbolic reference is one kind of item that may
appear in the constant pool. Java virtual machine instructions that use a symbolic reference specify the index
in the constant pool where the symbolic reference resides. For example, the
getstatic opcode, which pushes the value of a static field onto the stack, is followed
in the bytecode stream by an index into the constant pool. The constant pool entry at the specified index, a
CONSTANT_Fieldref_info entry, reveals the fully qualified name of the class in
which the field resides, and the name and type of the field.
Keep in mind that the Java virtual machine contains a separate runtime constant pool for each class and interface it loads. When an instruction refers to the fifth item in the constant pool, it is referring to the fifth item in the constant pool for the current class, the class that defined the method the Java virtual machine is currently executing.
Several instructions, from the same or different methods, may refer to the same constant pool entry, but each constant pool entry is resolved only once. After a symbolic reference has been resolved for one instruction, subsequent attempts to resolve it by other instructions take advantage of the hard work already done, and use the same direct reference resulting from the original resolution.
Linking involves not only the replacement of symbolic references with direct ones, it also involves
checking for correctness and permission. As mentioned in Chapter 7, "The Lifetime of a Class," the checking
of symbolic references for existence and access permission (one aspect of the full verification phase) is
performed during resolution. For example, when a Java virtual machine resolves a
getstatic instruction to a field of another class, the Java virtual machine checks to
make sure that:
If any of these checks fail, an error is thrown and resolution fails. Otherwise, the symbolic reference is replaced by the direct reference and resolution succeeds.
As described in Chapter 7, "The Lifetime of a Class," different implementations of the Java virtual
machine are permitted to perform resolution at different times during the execution of a program. An
implementation may choose to link everything up front by following all symbolic references from the initial
class, then all symbolic references from subsequent classes, until every symbolic reference has been resolved.
In this case, the application would be completely linked before its
main() method was
ever invoked. This approach is called early resolution. Alternatively, an implementation may
choose to wait until the very last minute to resolve each symbolic reference. In this case, the Java virtual
machine would resolve a symbolic reference only when it is first used by the running program. This approach
is called late resolution. Implementations may also use a resolution strategy in-between these
Although a Java virtual machine implementation has some freedom in choosing when to resolve symbolic references, every Java virtual machine must give the outward impression that it uses late resolution. No matter when a particular Java virtual machine performs its resolution, it will always throw any error that results from attempting to resolve a symbolic reference at the point in the execution of the program where the symbolic reference was actually used for the first time. In this way, it will always appear to the user as if the resolution were late. If a Java virtual machine does early resolution, and during early resolution discovers that a class file is missing, it won't report the class file missing by throwing the appropriate error until later in the program when something in that class file is actually used. If the class is never used by the program, the error will never be thrown.