Observing the Dynamic Linking Process in Java
Sophia Drossopoulou and
Susan Eisenbach
Java supports a novel paradigm for code deployment: Instead of linking the complete program into code before execution, the classes and interfaces making up the program are loaded and linked on demand during execution. Classes are verified before the creation of objects. Verification checks subtypes, and may require loading of further classes or interfaces.
This is a more complex runtime model than usually found in programming languages, but it has the advantage of faster strart-ip (as there is less code to load initially), of linking at runtime to the most up-to-date version of any utility, and of lazier error detection (exceptions only need to be thrown if there is an attempt to execute unsafe code). These advantages are obtained without compromising type safety. Usually, the Java linking process takes place implicitly, and, as long as it "goes well", it does not manifest itself, and does not affect program evaluation. Thus, dynamic linking is normally transparent to Java programmers.
Nevertheless, it is not always transparent to Java programmers, even if the programmers do not use low-level features such as reflection or explicit class loading: During program execution it is possible to encounter load errors or verification errors, and, if the verifier is switched off , type safety may be violated . Therefore, it is necessary for programmers to have an understanding of this mechanism, even if they do not use it explicitly.
The Java Language Specification contains many small program examples which elucidate Java language features. Gilad Bracha, one of the authors, has stated that almost all of the Language Specification is written as if programs were compiled and linked in the conventional rather than in a dynamic manner. Dynamic linking is described only in chapter 12, Execution . This chapter, unlike most of the rest of the Specification, contains only a few examples, which demonstrate issues around initialization only. Also, although several papers formalize verification, and the overall dynamic linking process, to our knowledge, there exists no introduction in terms of examples.
The current page aims to fill this gap. We explain the Java dynamic linking process though a sequence of examples, where dynamic linking manifests itself. Each example demonstrates one feature only, and consists of two or more classes, and their corresponding output. Dynamic linking manifests itself either through the trace of class loading (execution in verbose mode, i.e. ), or through loader, verification and resolution exceptions, or through erroneous execution, when the verifier was turned off. For each example, we state the order in which classes need to be compiled. All examples have been executed using JDK 1.3, and in verbose mode ( -verbose ). In most examples it makes no difference whether the verifier is on or off; if it does, we state explicitly what state it should be in is given ( e.g . execution with -noverify ).
These pages consist of a brief introduction to the phases of dynamic linking , and their dependencies . This is followed by brief descriptions of each of the phases ( verification , preparation , resolution , lo ading ) with examples. Finally, a larger example puts the phases together again, and demonstrates the dependencies across phases.
It is the intention that the descriptions of the phases can be read separately and in any order.
Java program execution is in terms of several different kinds of entities. These are:
Each phase has certain
effects, and expects certain conditions to hold, and otherwise
throws an exception. We describe this in the following table
The five phases, with possible
exceptions
| phase | possible exceptions |
| evaluation of term | If language rules broken, then
NullPointerEexception, ArrayStoreException, ArrayIndexOutOfBoundsException, ArithmeticException etc., etc.,... Also, user defined exceptions may be thrown explicitly |
| loading of type T | If a type is not found, or badly
formed, then: ClassCircularityError ClassFormatError NoClassDefFoundError |
| verification of type T | If verification not successful,
then: VerifyError |
| preparation of a type T | If there is not enough memory
for preparation to take place, then OutOfMemory |
| resolution of symbolic reference | If symbolic reference
is invalid, then IncompatibleClassChangeError IllegalAccessError InstantiationError NoSuchFieldError NoSuchMethodError UnsatisfiedLinkError |
Each type goes through the loading, verification and preparation phases in this order, but phases of one type may be interleaved with phases of another type, because when a type goes through a phase it may require another type to have been through another phase. The following diagram demonstrates these issues:
Thus, execution switches between the different
phases for different kinds or terms. Loading, verification and preparation
take place consecutively for a particular type, but not necessarily for
all types together, nor do they take place without interruption: between
loading and verification of a class there may be execution of other parts
of a program. Also, verification may be skipped (the "parallel"
red arrow),
although in such a case, there can be no guarantees of type safety.
An example
The following example demonstrates
dependencies of linking phases, and their interleaving. Consider the
following classes:
class A{ int i; }
class B extends A{ }
class C{
void
f(){ }
void g(B b){ b.i=10; }
}
class Test{
public static
void main(String[] args){
C c; B b;
c= new
C();
c.f();
b =
new B();
c.g(b);
}
}
Execution of the code from above, in a setting where A , B and C have not been loaded yet, and where loading and verification is lazy, would activate the following linking phases, where we dictincguish between the case where the verifier is on, and the case where the verifier is off:
| code |
linking phases, with verifier on |
linking phases, with verifier off |
|
| 1. |
c=new C(); |
load
C load A load B verify C prepare C evaluation: create new C object |
load
C prepare C evaluation: create new C object |
| 2. |
c.f(); |
resolve method
void f() from class C evaluation: execute above method with receiver c |
resolve method
void f() from class C evaluation: execute above method with receiver c |
| 3. |
b=new B(); |
verify
A verify B prepare A prepare B evaluation: create new B object |
load A loab B prepare A prepare B evaluation: create new B object |
| 4. |
c.g(b); |
resolve method
void g(B) from class C evaluation: execute above method with receiver c and argument b resolve field int i from class A evaluation: assignment to above field of b |
esolve method
void g(B) from class C evaluation: execute above method with receiver c and argument b resolve field int i from class A evaluation: assignment to above field of b |
Namely, in the case of
the verifier being turned on.
1. For the creation of the new C object, the class C needs to be prepared, which requires C to be verified. That, in turn, requires verification of the method body, where a field defined in class A is accessed from , an object of class B. This requires establishing that B is a subclass of A, which, in turn requires loading of A and B.
2. The method call c.f() requires resolution of the method void f() defined in class C. Execution of the method requires nothing further, since the method body is empty .
3. For the creation of the new B object, the class B needs to be prepared, which requires B to be verified, which in turn requires A, its superclass, to be verified . Verification of classes A and B poses no further requirements, since these classes do not declare any methods.
4. The method call c.g(b) requires resolution of the method void g() defined in class C. Execution of the method body requires resolution of the field int i defined in class A, which returns a field offset. This offset is used to find the appropriate field in the object b , and to assign to it the value 10.
On the other hand, in the
case of the verifier being turned off:
1. For the creation of the new C object, the class C needs to be prepared, which requires C to be loaded
2. The method call c.f() requires resolution of the method void f() defined in class C. Execution of the method requires nothing further, since the method body is empty .
3. For the creation of the new B object, the class B needs to be prepared, which requires B to be verified, which in turn requires A, its superclass, to be verified . This, again, requires loading of A and B.
4. The method call c.g(b) requires resolution of the method void g() defined in class C. Execution of the method body requires resolution of the field int i defined in class A, which returns a field offset. This offset is used to find the appropriate field in the object b, and to assign to it the value 10.
Additional clarifiecation can be obtained from the following sections,
where we discuss each phase (
verification
,
preparation
,
resolution
,
loading
) idividually,
and give examples that show the phases
manifesting
themselves. At the end of these pages, we give a
longer example
, which further illustrates the interleaving caused by the dependencies
of the phases,
and which illustrates the alternatives between eager and lazy linking and
loading, allowed by the Java Language Specification.
Loading is the process of finding the binary of a class or interface of a particular name. Classes and interfaces are loaded if they are required for execution or if they are required in order to establish a subtype relationship.
The activity of a loader can be observed through the verbose flag in execution (-verbose ) and through errors when classes are not found. When loading a class all its superclasses and interfaces are loaded . When loading an interface, its superinterfaces are loaded . Classes are only loaded once.
Loading fails if the appropriate classes/interfaces cannot be found, the classes have a class circularity or are badly formed. Then the following errors may be thrown:
Class loaders find the binary form of a class or interface and construct class objects to represent them. A bootstrap class loader is provided by the Java Virtual Machine. User defined class loaders can also be written. Class loaders can delegate the loading of the class or interface to another class loader.Type safe linkage is maintained by using both the loader and the class/interface name to identify a loaded class object. Loading constraints of the form <classname, loader1> = <classname,loader2> are imposed during preparation and resolution.
A class C has to have been loaded when verifying a new C instruction, a method call or field access of a method or filed declared in C .
Later on, we also give examples for delegating loaders, i.e. examples forThe activity of a loader can be observed through executing the bytecode interpreter in verbose mode ( -verbose). In the following example, class A is loaded before the creation of the first A object, and class B is loaded before the creation of the first B object:
class A{}
class B{}
class Test{
public static
void main(String[] args){
System.out.println(0);
B b =
new B();
System.out.println(1);
A a1 =
new A();
System.out.println(2);
A a2 =
new A();
System.out.println(3);
}
}
Partial output from executing Test:
[Loaded Test]
0
[Loaded B]
1
[Loaded A]
2
3
The above example also shows that a class is loaded only once, even if more than one instance of that class is created.
When loading a class, all its superclasses will be loaded. In the following example, loading C requires its superclasses, i.e. B and A , to have been loaded:
class A{}
class B extends A{}
class C extends B{}
class Test{
public static
void main(String[] args){
C c =
new C();
}
}
Partial output from executing Test:
[Loaded Test]
[Loaded A]
[Loaded B]
[Loaded C]
When loading a class which implements an interface, both the class and its interface will be loaded. In the following example, loading A forces the loading of its interface I:
interface I{}
class A implements I{}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
Partial output from executing Test:
[Loaded Test]
[Loaded I]
[Loaded A]
Loading an interface loads all its superinterfaces:
interface I{}
interface J extends I{}
class A implements J{ }
class Test {
public static
void main(String[] args){
System.out.println(0);
A a = new
A(); }
}
Partial output from executing Test:
[Loaded Test]
0
[Loaded I]
[Loaded J]
[Loaded A]
If a class file is removed after it has been compiled, then a NoClassDefFoundError exception will be thrown. In the following example, an object of class A is created at *. After compiling all the code, the file A.class is removed. Test is not recompiled, it is only executed. As there is no class file for A, a NoClassDefFoundError exception is thrown.
class A{}
class Test{
public static
void main(String[]
args){
A a = new A();
//*
}
}
Test run - partial output from executing Test :
[Loaded Test]
[Loaded A]
The file
A.class is
deleted, and Test
is not recompiled.
Partial output from executing
Test:
[Loaded Test]
... java.lang.NoClassDefFoundError: A at Test.main
The exception ClassCircularityError will be thrown if loading the supertypes of a type encounters a circularity in the subtype hierarchy. In the following example, B is a superclass of A and A is a superclass of B . This is achieved in two phases, namely we first define A to be a class, and B as its subclass, and then we recompile A , with B as its superclass:
First, compile:
class A{}
class B extends A{} //*
class Test{
//*
public static
void main(String[] args){ A a =
new A(); }
}
Then, in another directory compile:
class B {}
class A extends B{} //*
Then, overwrite the original A.class with the new A.class , leaving the old B.class as it was. Thus, in the original directory we have the class files from the Java classes marked with * .
Partial output from executing Test:
[Loaded Test]
... java.lang.ClassCircularityError: A
ClassFormatError will be thrown if the class file is badly formed.
First, compile the following classes:
class A{}
class Test{
public static
void main(String[] args){ A a =
new A(); }
}
The file A.class is then corrupted (for example by altering it in a text editor).
Partial output from executing Test:
[Loaded Test]
... java.lang.ClassFormatError: A
The Java Language Specification states:
The lesson is that an implementation that lacks a verifier or fails to use it will not maintain type safety and is, therefore, not a valid implementation.
Verification checks that the loaded
representation of each class is well formed. If an error occurs during
verification, then an instance of the subclass of
VerifyError
will be thrown at the point
in the program that caused the class to be verified:
VerifyError: The binary definition for a class or interface failed to pass a set of required checks ... and that it cannot violate the integrity of the Java virtual machine.Verification establishes that a type is a subtype of another type. To do this, it checks the required subtype relationship against the loaded or prepared code and throws a verification exception if the relation is not satisfied. If the types, classes or interfaces, involved in the subtype relationship have not yet been loaded, then the verifier will attempt to load them in order to check the subtype relation. If the types cannot be loaded then a load error will be thrown.
Verification can be turned off with the -noverify execution directive, in which case type safety cannot be guaranteed.
The verifier can be observed in two ways: Either by observing which classes are being loaded , or by observing the situations where a verification error is thrown .
The verifier is called to verify a class before an object of that class is created . It checks that class and all of its superclasses , but does not check any further classes used .
The verifier checks that
The verifier works on bytecode, which contains some but not all of the type information available in the original Java code. Thus, the bytecode contains the signatures of method declarations, method calls and field accesses are enriched with appropriate descriptors. On the other hand, the bytecode does not contain the types of local variables.
Thus, the verifier
When the verifier needs to check that t1 is a subtype of t2 , it may need to load t1 and t2 , if they have not already been loaded.
In the following example, an object of class A is created at *. A contains a method m . Within m, at ** , the method m1( ) , defi ned in class B , is called for an object of class C . The bytecode for the method call shows that the method void m() from class B is called. The verifier needs to ascertain that C indeed inherits that method from B , i.e that C is a subclass of B (even though m is not actually called), and for this, it loads the classes B and C. When running the example with verification off, B and C are not loaded , which demonstrates that it is the verification that causes the loading.
Partial output from executing Test with verification on:
[Loaded Test]
[Looaded A]
[Loaded B]
[Loaded C]
[Loaded Test]
[Loaded A]
When the verifier needs to check that t1 is a subtype of t2, if it finds that it is not, it throws a verification error.
This example starts with the same code as the previous example: Within m , at * , the method m1( ), defi ned in class B , is called for an object of class C . The bytecode for the method call shows that the method void m() from class B is called. The verifier needs to ascertain that C indeed inherits that method from B , i.e that C is a subclass of B (even though m is not actually called), and for this, it loads the classes B andC. After compiling all the code, C.class and B.class are removed. The verifier attempts to load both B and C , even though m is not actually called. Since they cannot be found, an exception is thrown. Running the same example with the verifier off shows that it is the verifier that causes this attempt at early loading.
Partial output from executing Test with verification on with B.class and C.class removed:
[Loaded Test]
[Loaded A]
java.lang.NoClassDefFoundError: B
[Loaded Test]
When the verifier needs to check that t1 is a subtype of t2 , if t1 has already been loaded, it is not reloaded.
In the following example, the creation of an E object at * requires preparation and thus verification of class E. Verification of class E requires B to be a subclass of A and thus causes A and B to be loaded. The creation of an F object at ** requires preparation and thus verification of class F. Verification of class F requires C to be a subclass of A and thus causes C to be loaded. Note that it does not need to load A , since A had been loaded previously.
In the following
example, verification of class E
manifests itself through the loading of classes
A and
B, whereas verification of
class F
manifests itself through the loading of classes
C and
A . When the verifier
is on, it is called both for F
and for
E before the creation of
the F
object at *
,
and in the process it loads classes A
, B, and
C.
Partial output from executing
Test
, with the verifier on:
[Loaded Test]
0
[Loaded E]
[Loaded F]
[Loaded A]
[Loaded B]
[Loaded C]
1
Partial output from executing Test, with the verifier off:
[Loaded Test]
0
[Loaded E]
[Loaded F]
1
Verification of a class does not involve verification of the classes loaded when verifying that class. Here, the creation of an object of class A requires previous verification of class A . Verification of class A needs to establish that C is a subclass of B , and so it loads B and C . However, verification of A does not need to verify class B and thus does not need to establish that E is a subtype of D . Therefore, the following program does not load D and E.
class D{int i;}
class E
extends D{}
class B{
void
m1(){
int j = new
E().i;
}
}
class C
extends B{}
class A{
void
m(){
new C().m1();
}
}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
Partial output from executing Test with verification on:
[Loaded Test]
[Loaded A]
[Loaded B]
[Loaded C]
At line * the field i , declared in class B, is accessed through an object of class C. The verifier needs to establish that C is a subtype of B.
class B{int i = 0;}
class C extends B{}
class A{
void
m(){
int j =
new C().i; //
*
}
}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
Partial output from executing Test with verification on:
[Loaded Test]
[Loaded A]
[Loaded B]
[Loaded C]
Partial output from executing Test with verification off:
[Loaded Test]
[Loaded A]
At line * the method m1, declared in class B, is executed by an object of class C. Verification of class A needs to establish that C is, indeed, a subtype of B.
class B {
void m1(){}}
class C
extends B{}
class A{
void
m(){
new C().m1(); /*
}
}
class Test{
public static
void main(String[] args){
A a = new
A();
}
}
Partial output from executing Test with verification on:
[Loaded Test]
[Loaded A]
[Loaded B]
[Loaded C]
Partial output from executing Test with verification off:
[Loaded Test]
[Loaded A]
At line * the method m1, declared in class Test, with formal parameter type A is called with an actual parameter of type B. The verifier needs to establish that B is, indeed, a subtype of A.
class A{}
class B
extends A{}
class Test{
public void m1(A x){}
public
void m2(){
m1(
new B()); // *
}
public static
void main(String[] args){}
}
Partial output from executing Test with verification on:
[Loaded Test]
[Loaded A]
[Loaded B]
Partial output from executing Test with verification off:
[Loaded Test]
Due to the differences in information between Java code and bytecode (bytecode does not contain the types of local variables), the verifier does not need to check subtypes for assignments to local variables, or to parameters.
In the following example, at * an object of class B is assigned to a local variable of type A. The verifier, as we know from previous examples, has to verify all methods in Test, and thus needs to verify the assignment as well. As we see, the verifier does not attempt to establish that B is a subtype of A .
class A { }
class B
extends A{}
class Test{
public static
void main(String[] args){}
void m( ){
A a = new
B(); // *
}
}
Partial output from executing Test with verification on (or off):
[Loaded Test]
[Loaded C]
The verifier is optimistic
when checking whether a type exists, or, in other words, whether a
T
is a subtype of itself. Here, class
Test contains field accesses
and method calls from class A
. However, verification only needs to establish that
A is a subtype of
A, so does not load
A .
The verifier does not load A to check its existence. Neither field
access, *,
nor method call,
**
, cause the loading of class
B .
Partial output from executing Test with verification on (or off):
[Loaded Test]
The verifier is optimistic when checking whether a T is a subtype of an interface. Here, at line ** the method m1, declared in class Test, with argument type interface I , is called at * with an actual parameter of type class A. The verifier does not establish that A implements I.
interface I{}
class A implements I{}
class Test{
public
static
void m1(I i){}
public
static
void main(String[] args){
m1(
new A()); //*
}
}
Partial output from executing Test:
[Loaded Test]
To see that there is no check that A implements I, alter class A
class AA
{}
class A
extends AA{}
and recompile A, without recompiling Test . Partial output from executing Test with verification on:
Verification guarantees wellformedness of the state. If it is turned off, or fooled (earlier versions of the verifier had some holes), then any part of the memory may be accessed, and the system may be brought to an inconsistent state.
In our example, a field from a superclass is accessed, and then the subclass is changed so as not to be a subclass of the original class. Then, with verification on, a VerifyError is thrown. However, if the verifier is not called, then other parts of memory may be accessed.
class A { int i = 0; }
class B extends A{ int j = 1;}
class Test{
public static void main(String[]
args) {
System.out.println(
new B().i);
}
}
If class B is changed :
class B{ int j = 1;}
The modified
class B
is compiled, and
Test is not recompiled.
Partial output from executing
Test
with verification on:
[Loaded Test]
[Loaded A]
[Loaded B]
... java.lang.VerifyError: (class: Test, method:
main ...) Incompatible type for getting or setting field
Partial output from executing Test with verification off:
[Loaded Test]
[Loaded B]
[Loaded A]
1
Thus, with the verification off, we ended up accessing the wrong field, and no exception was raised. This happens because the layout of superclasses is a prefix of the layout of subclasses. When accessing a filed defined in a superclass, from an object belonging to a subclass, the offset of the field as defined in the superclass is used - this allows for faster field access, because resolution need only be applied the first time the filed access is executed, and from then on, the same offset may be used. However, in the above example, new B() , the object from which the field is accessed, does not belong to a subclass of A , the class containing the field, and so, the offset is invalid for that object.
PreparationPreparation consists of determining the object layout and creating a method lookup table. A method lookup table contains enough information to allow the appropriate method to be invoked without having to look at superclasses.
In addition, during preparation class variables and constants are created and initialized to their default values. During preparation an OutOfMemory may be thrown, if there isn't enough memory to create the method lookup table.
The IBM Java system informs the user when preparation is occurring when the verbose option is on.
ResolutionBinary files can contain symbolic references to other classes, fields, methods and interfaces. These symbolic references are fully qualified. For fields and methods these references contain the name of the field or method, appropriate type information and the names of the class or interface where the declaration occurred. Resolution checks that the reference is correct and may replace it with a direct reference.
Resolution will fail if it attempts to resolve:
If an attempt is made to create an object of a class which is an interface, then an InstantationError will be thrown. This could happen if a class is compiled, a second class which creates an object of the first class is compiled, the first class is changed to be an interface and the first class is recompiled. The second class is not recompiled. For example, compile class A and Test :
class A{}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
If the class A is then replaced by an interface A
interface A{}
A is recompiled, but not Test.
Partial output from executing
Test:
[Loaded Test]
[Loaded A]
... java.lang.InstantiationError: A
If an attempt is made to create an object of a class which implements an interface, that no longer exists as an interface, then InstantationError will be thrown. This could happen if an interface were compiled, a class that implements the interface is compiled, a second class which creates an object of the first class is compiled, the interface is changed to be a class and then is recompiled. The other classes are not recompiled. For example, compile interface I , class A and Test :
interface I{}
class A implements I{}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
The interface I is then turned into a class:
class I{}
I
is recompiled, but not
Test.
Partial output from executing
Test
:
[Loaded Test]
[Loaded I]
... java.lang.IncompatibleClassChangeError: Implementing
class
If an attempt is made to create an object of a class which is abstract, the an InstantiationError is thrown. This could happen if a class were compiled, a second class which creates an object of the first class is compiled, the first class is changed to be abstract and the first class is recompiled. The second class is not recompiled. For example, compile class A and Test :
class A{}
class Test{
public static
void main(String[] args){
A a =
new A();
}
}
Then the class A is made abstract :
abstract class A{}
A
is recompiled, but Test
is not.
Partial output from executing
Test:
[Loaded Test]
[Loaded A]
... java.lang.InstantiationError: A
If resolution attempts to access
a field from a given type, but the type does not contain the field, then the
exception NoSuchFieldError
is thrown. This could
happen if a class with a field were compiled, a second class which accesses
the field is compiled, the field from the first class is removed
and the first class is recompiled. The second class is not recompiled.
For example, compile class
A and
Test :
class A{
int i
= 1;
int j
= 2;
int k
= 3;
}
class Test{
public static
void main(String[] args){
A a =
new A();
System.out.println(0);
System.out.println("j
= " + a.j);
}
}
Change type of field j in class A:
class A{
int i
= 1;
char j = 'j';
int k
= 2;
}
A
is recompiled, but Test
is not.
Partial output from executing
Test:
[Loaded Test]
[Loaded A]
0
... java.lang.NoSuchFieldError: j
Note, that neither the field String j , not the field i preceding j , nor the field k following j were confused for the field int j .
If resolution attempts to access a field from a given type, but the type does not contain the field, then the exception NoSuchFieldError is thrown. This could happen if a class with a method were compiled, a second class which calls the method is compiled, the method from the first class is removed and the first class is recompiled. The second class is not recompiled. For example, compile class A and Test :
class A{ void m(){} }
class Test{
public static
void main(String[] args)
{ A
= new A(); System.out.println(0);
a.m(); }
}
Method m is removed
class A{}
A is recompiled, but not Test . Partial output from executing Test :
[Loaded Test]
[Loaded A]
0
... java.lang.NoSuchMethodError
There is a reference to a field or method which at the time that the code containing the reference was compiled, had been declared as visible (e.g. public or default) and later it was changed to be not visible. The exception IllegalAccessError will be thrown during resolution only if the verifier has been called previously.
class A{int i = 0;}
class Test{
public static
void main(String[] args){
A a =
new A();
System.out.println(a.i);
}
}
test run - partial output from executing Test :
[Loaded Test]
[Loaded A]
0
Then the field i is made private and A is recompiled:
class A{private int i = 0;}
test run - partial output from executing Test with verification on:
[Loaded Test]
[Loaded A]
java.lang.IllegalAccessError: try to access field A.i
from class Test
at Test.main(Test.java:4)
test run - partial output from executing Test with verification off:
[Loaded Test]
[Loaded A]
0
Note that with the verifier off, a public access to a private field can occur.
There is a reference to a method written in another language and this method is no longer available.
This is the code for the delegating
loader used in the multiple loader examples. This delegating loader
takes as input parameters, the name of the loader (
id ), the directory to
load classes from (dir
), a set of classes that the loader can load (
classNames) and the loader
that should be used (next
) if the class to be loaded is not in the set of
classNames. If the class
to be loaded is also not in next
loader's set of classes that it can load, then an attempt is made
to load using the system loader.
This example demonstrates the use of three different loaders to load a program. Classes A and Test are loaded by loader1. Class B is loaded by loader2. The system loader loads all other classes. These include all of the Java classes and class C .
This program consist of five classes. Test , A , C , and the delegating loader (above) are in the current directory. The class B is in the subdirectory dir . To compile A.java , a class B.class must exist in the same directory.
//based on test programs written by Saraswat and Coglio
output from executing
Test with verification
on and verbose off:
Type safe linkage is maintained by using both the loader and the class/interface name to identify a loaded class object. This example demonstrates that within a program two different classes with the same name cannot both be loaded. This program loads A.class with loader1 and then attempts to load a different A.class (from /dir ) with loader2 . The loading constraints < A , loader1> = < A , loader2> is not met and so an exception is thrown:
LinkageError: Class A violates loader constraints
This program consist of five classes. Test , A , and the delegating loader (above) are in the current directory. The class B and a different class A are in the subdirectory dir .
//based on test programs written by Saraswat and Coglio
partial output from executing
Testwith verification
on and verbose off:
In the following example we demonstrate the dependencies across phases.
Consider the
following classes:
class
A{
public
void m2( ){ }
}
class
B extends
A{
public
void m3( ){
new C().m2();
}
}
class
C extends
A{ }
class
D{
public
void m1( ){ } }
class
Test{
public static
void main(String[] args){
new
D().m1();
new
A().m2();
}
void
g( ){
new B().m2();
}
}
Execution
of the program Test.Main
: requires the following evaluation steps to be applied in the
following order:
call Test.main(),The above steps are a straightforward reflection of the code, and do not consider the linking phases involved. The order of execution is shown in the figure in the right, where the blue, broken arrow indicates the order of evaluation, i.e.
|
|
| However,
as we discussed earlier, evaluation steps have their own requirements
from the linking phases. For example, calling
Test.main() requires
Test to have been prepared,
which requires Test
to have been verified. Verification of
Test requires
Test to have been loaded, and also
requires B
to be a subclass of A
. Establishing the latter requires
A and
B to have been loaded
(although verifiers may do this by posting constraints instead). Loading
B
requires A
to have been loaded previously.
These, and the remaining linking related dependencies are illustrated in the figure to the right. Here
again, the blue the broken blue arrows express constraints imposed by the
program code, whereas the read arrows express constraints
|
|
The Specification
does not totally constrain the execution sequence. In the
interrelationship example
, class B
is loaded for the verification of class
Test but it need not be
verified. Also, class C
, although mentioned in class
B doesn't need to be loaded
if class B
has not been verified. Two possible execution sequences are:
| lazy execution | eager execution | |
| load Test
load A load B verify Test prepare Test resolve Test.main()
evaluate Test.main() load D
evaluate new D() resolve D:m1()
evaluate D:m1() verify A prepare A evaluate new A() resolve A.m2() evaluate A.m2() |
|
load Test
load A load B verify Test
prepare A
resolve D.m1()
evaluate D.m1()
evaluate new A() resolve A.m2() evaluate A.m2() |
Gilad Bracha, Adventure in Computational Theology: Selected Experiences with the Java(tm) Programming Language, Invited talk at ECOOP Workshop on Formal Techniques for Java Programs, Budapest, June 2001.
Alessandro Coglio and Allen Goldberg, Type Safety in the JVM: Some Problems in Java 2 SDK 1.2 and Proposed Solutions, 2001. To appear in Concurrency: Practice and Experience.
Sophia Drossopoulou, Towards
a model of Java dynamic linking and verification,
Harper, R., (Ed.): (2001)
Types in Compilation · Third International Workshop, TIC
2000, Montreal, Canada, September 21, 2000. Revised Selected Papers
LNCS 2071, Springer Verlag,
2001
James Gosling, Bill Joy, Guy Steele, Gilad Bracha, The Java Language Specification, Addison Wesley, 2nd Ed (31 July, 2000).
Tim Lindholm, Frank Yellin, The Java Virtual Machine Specification, Addison Wesley Longman Publishing Co, 2nd Ed (1 July, 1999).
Zhenyu Qian, Allen Goldberg, Alessandro Coglio, A Formal Specification of Java Class Loading , Proc. OOPSLA 2000, April 2000.
Vijay Saraswat, Java is
not type-safe, Web pages at: http://www.research.att.com/~vj/main.html,1997.