java基础之Classloading and class objects

来源:互联网 发布:分析数据 编辑:程序博客网 时间:2024/05/20 05:06

java基础之Classloading and class objects

A.class file defines a type for the JVM, complete withfields, methods, inheritance
information, annotations, and other metadata.
The class file format is well-described
by the standards, and any language that wants to run on the JVM must adhere to it.
The class is the smallest unit of program code that the platform can load. In order
to get a new class into the current execution state of the JVM, a number of steps must
be performed. First, a class file must be loaded and linked, and then it must beextensively verified. After this, a new Class object representing the type will be available to
the running system, and new instances can be created.
 

Overview—loading and linking

The purpose of the JVM is to consume class files and execute the bytecode they contain. To do so, the JVM must retrieve the contents of the class file as a data stream of bytes, convert it to a useable form, and add it to the running state. This two-step
process is referred to as loading and linking (but linking breaks down into a number of subphases).

LOADING

The first step is to take the data stream of bytes that constitute the class file and to thaw out this frozen representation of the class. This process starts with a byte array (often read in from a filesystem) and produces a Class object that corresponds to the class you’re loading. During this process, some basic checks are performed on the class, but at the end
of the loading process, the Class object isn’t fully fledged, and the class isn’t yet usable.

LINKING

After loading, the class must be linked. This step breaks down into three subphases—
verification, preparation, and resolution. Verification confirms that the class file conforms to expectations and won’t cause runtime errors or other problems for the running system. After this, the class is prepared, and all other types referenced in the class file will be located to ensure that this class is ready to run.
This relationship between the phases of linking can be seen in figure 5.1.




Verification

Verification can be quite a complex process, consisting of several stages.
First is a basic integrity check. This is really part of loading and ensures that the class file is sufficiently well-formed to attempt to link.
Then comes a pass that checks that the symbolic information contained in the constant pool (discussed in detail in section 5.3.3) is self-consistent and obeys the basic behavior rules for constants. Other static checks that don’t involve looking at the code (such as checking that final methods aren’t overridden) are also performed at this time.
After this comes the most complex part of verification—checking the bytecode of
methods. This involves ensuring that the bytecode is well-behaved and doesn’t try to
circumvent the VM’s environmental controls. The following are some of the main
checks that are performed:
■ Check that all methods respect access control keywords
■ Check that methods are called with the right number of parameters of the correct static types
■ Make sure that the bytecode doesn’t try to manipulate the stack in evil ways
■ Ensure that variables are properly initialized before they’re used
■ Check that variables are only assigned suitably typed values

These checks are done for performance reasons—they enable the skipping of runtime checks, thus making the interpreted code run faster. They also simplify the compilation of bytecode into machine code at runtime (this is just-in-time compilation,
which we’ll cover in section 6.6).
PREPARATION
Preparing the class involves allocating memory and getting static variables in the class
ready to be initialized, but it doesn’t initialize variables or execute any VM bytecode.
RESOLUTION
Resolution causes the VM to check that every type referred to by the new class file is
known to the runtime. If the types aren’t known, they may also need to be loaded.
This can kick off the classloading process again for any new types that have now
been seen.
Once all additional types that need to be loaded have been located and resolved,
the VM can initialize the class it was originally asked to load. In this final phase, any
static variables can be initialized and any static initialization blocks run—you’re now
running bytecode from the newly loaded class. When this completes, the class is fully
loaded and ready to go.



Class objects


The end result of the linking and loading process is a Class object, which represents the newly loaded and linked type. It’s now fully functional in the VM, although for performance reasons some aspects of the Class object are only initialized on demand.
Your code can now go ahead and use the new type and create new instances. In addition, the Class object of a type provides a number of useful methods, such as getSuperClass(), which returns the Class object corresponding to the supertype.
Class objects can be used with the Reflection API for indirect access to methods, fields, constructors, and so forth. A Class object has references to Method and Field objects that correspond to the members of the class. These objects can be used in the Reflection API to provide indirect access to the capabilities of the
class. You can see the high-level structure of this in figure 5.2.
 


So far, we haven’t discussed exactly which part of the runtime is responsible for locating and linking the byte stream that will become the newly loaded class. This is handled by classloaders—subclasses of the abstract class ClassLoader, and they’re our next subject.

Classloaders

The platform ships with a number of typical classloaders, which are used to do different jobs during the startup and normal operation of the platform:

Primordial (or bootstrap) classloader—This is instantiated very early in the process
of starting up the VM, and is usually implemented as native code. It’s often best
to think of it as being a part of the VM itself. It’s typically used to get the basic
system JARs—basically rt.jar—loaded and it does no verification.
Extension classloader—This is used to load installation-wide standard extensions.
This often includes security extensions.
Application (or system) classloader—This is the most widely used classloader. It’s
the one that will load the application classes and do the majority of the work in most SE environments.
Custom classloader—In more complex environments, such as EE or the more sophisticated SE frameworks, there will often be a number of additional (a.k.a. custom) classloaders. Some teams even write classloaders that are specific to their individual applications.

In addition to their core role, classloaders are also often used toload resources (files
that aren’t classes, such as images or config files) from JAR files or other locations on
the classpath.


Example—an instrumenting classloader
One simple example of a classloader that transforms as it loads is the one used in the
EMMA testing coverage tool. EMMA is available from http://emma.sourceforge.net/.
EMMA’s classloader alters the bytecode of classes as they’re loaded to add extra
instrumentation information. When test cases are run against the transformed code,
EMMA records which methods and code branches are actually tested by the test
cases. From this, the developer can see how thorough the unit tests for a class are.
We’ll have more to say about testing and coverage in chapters 11 and 12.




It’s also quite common to encounter frameworks and other code that makes use of specialized (or even user-defined) classloaders with additional properties. These will frequently transform the bytecode as it’s being loaded, as
we alluded to in chapter 1. In figure 5.3, you can see the inheritance hierarchy of classloaders, and how the different
loaders relate to each other.




Let’s take a look at an example of a specialized classloader and look at how classloading can be used to implement DI.


Example—classloaders in Dependency Injection

The core idea of DI is twofold:
■ Units of functionality within a system have dependencies and configuration information upon which they rely for proper functioning.
■ The dependencies are usually difficult or clumsy to express within the context of the objects themselves.
The picture that should be in your head is of classes that contain behavior, and configuration and dependencies that are external to the objects. This latter part is what is usually referred to as the runtime wiringof the objects.
In chapter 3, we met the Guice framework as an example of DI. In this subsection, we’ll discuss how a framework could make use of classloaders to implement DI. However, the approach we’ll discuss in this example is quite different from Guice. In fact, it’s like a simplified version of the Spring framework.
Let’s look at how we’d start an application under our imaginary DI framework:

java -cp <CLASSPATH> org.framework.DIMain /path/to/config.xml

The CLASSPATH must contain the JAR files for the DI framework, and for any classes that
are referred to in the config.xml file (along with any dependencies that they have).
Let’s adapt an example we’ve already met before to this style. The service shown in
listing 3.7 is easy to adapt—and the result is shown in listing 5.1.


public class HollywoodServiceDI {private AgentFinder finder = null;public HollywoodServiceDI(){}public void setFinder( AgentFinder finder ){this.finder = finder;}public List<Agent> getFriendlyAgents(){...}public List<Agent> filterAgents( List<Agent> agents, String agentType ){...}}


For this to be managed under DI, you’ll need a config file too, like this:
<beans><bean id="agentFinder" class="wgjd.ch03.WebServiceAgentFinder"... /><bean id="hwService" class="wgjd.ch05.HollywoodServiceDI"p:finder-ref="agentFinder"/></beans>


In this approach, the DI framework will use the config file to determine which objects to construct. This example will need the hwService and agentFinder beans, and the framework will call the void constructor for each bean, followed by the setter methods (for example, setFinder() for the AgentFinder dependency of HollywoodServiceDI).
This means that classloading occurs in two separate phases. The first phase (which is handled by the application classloader) loads the class DIMain and any classes that it refers to. Then DIMain starts to run and receives the location of the config file as a parameter to main().
At this point, the framework is up and running in the JVM, but the user classes specified in config.xml haven’t yet been touched. In fact, until DIMain examines the config file, the framework has no way of knowing what the classes to be loaded are.
To bring up the application configuration specified in config.xml, a second phase of classloading is required. This uses a custom classloader. First, the config.xml file is checked for consistency and to make sure it’s error-free. Then, if all is well, the custom classloader tries to load the types from the CLASSPATH. If any of these fail, the whole process is aborted.
If this succeeds, the DI framework can proceed to instantiate the required objects and call the appropriate setter methods on the created instances. If all of this completes OK, the application context is up and can begin to run.
We’ve briefly touched on a Spring-like approach to DI, which makes heavy use of
classloading. Many other areas of the Java technology space are big users of classloaders and related techniques. These are some of the best-known examples:
■ Plugin architectures
■ Frameworks (whether vendor or homegrown)
■ Class file retrieval from unusual locations (not filesystems or URLs)
■ Java EE
■ Any circumstance where new, unknown code may need to be added after the
JVM process has already started running


读书笔记:The Well-Grounded Java Developer

1 0