dynamic linking Observing the Dynamic Linking Process in Java Sophia Drossopoulou and Susan Eisenbach For our paper on this subject which includes information about JDK 1.4 linking download: Manifestations of Java Dynamic Linking. 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. return to top Introduction to the phases of dynamic linking Java program execution is in terms of several different kinds of entities. These are: loaded (not yet verified) code, verified and prepared code, the expression being evaluated, the objects created by evaluation and their contents. The Java Lalnguage Specification distinguishes the following five phases of execution, which are applicable to different parts of a program (some apply to types, some apply to individual expressions): Evaluation of an expression is the "ordinary" execution of a Java program; it is unaffected by the linking processes. Loading of a type T finds the binary representation of T and turns it into a class object, (may use several class loaders). Verification of a type T checks the format of the bytecode, that the destination of jumps are correct, that there is no stack overflow or underflow, and that the subtype relations required in field accesses and method calls in T are satisfied. Preparation of a type T creates method and field tables to avoid the necessity of searching superclasses at invocation time, and initializes static fields. Resolution of a of a symbolic reference (i.e. method call or field access) replaces symbolic references to other classes and interfaces and their fields and methods with direct references. In accordance with Java terminology, we use the term type for interfaces and classes. Evaluation is the part of execution that is unrelated to the dynamic linking process, e.g. conditionals, assignment etc., and corresponds to these activities as found in most programming languages. The other four phases are directly related to dynamic linking. 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 Dependencies across the phases 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: loading a type T requires all supertypes of T to have been loaded, verifying T requires T to have been loaded, all supertypes of T to have been verified, and may require loading of further types T', if T' is part of a subtype relation which is required in some method body in T, preparing T requires all supertypes of T to have been prepared, and T to have been verified (unless the verifier is turned off), evaluation method call for a method defined in T requires resolution of that method which in its turn requires preparation of T; field access for a field defined in T requires resolution of that field, which in its turn requires preparation of T, creation of an object of class T requires preparation of T. In the above, the red arrows represent requirements, and the green , broken, arrow represents potential requirements, and the "parallel" red red arrow represents alternative requirements, i.e.: 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 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: NoClassDefFoundError ClassCircularityError ClassFormatError 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
= 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 for delegating loader used in these examples program whose classes are loaded by different loaders program whose classes are loaded by different loaders and which fails its loading constraints return to top Manifestation of the loader: classes are loaded The 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. return to loading return to top Manifestation of the loader: superclasses are loaded 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] return to loading return to top Manifestation of the loader: interfaces are loaded 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] return to loading return to top Manifestation of the loader: superinterfaces are loaded 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] return to loading return to top Manifestation of the loader: exception NoClassDefFoundError 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 return to loading return to top Manifestation of the loader: exception ClassCircularityError 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 return to loading return to top Manifestation of the loader: exception ClassFormatError 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 return to loading return to top Delegating loaders are discussed later Verification 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 bytecode is structurally correct the destination of each goto is a bytecode instruction the stack will not overflow or underflow the type rules of Java are respected The first three requirements are not directly visible to Java programmers and have more to do with establishing that the bytecode was, indeed, created by a Java compiler. The last requirement is however visible to Java programmers, when they recompile part of their code without recompiling the remaining, dependent code. Our examples demonstrate how the verifier checks this fourth requirement. 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 checks the inheritance hierarchy checks subtypes for field access checks subtypes for method calls receiver arguments does not check subtypes for assignments to local variables Furthermore, the verifier is lazy (or optimistic), where subtypes are required for interfaces or between the same type, and so does not check if t is a subtype of t does not check if t1 is a subtype of t2, when t2 is an interface return to to top Manifestations of the verifier: classes loaded 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. 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] [Looaded A] [Loaded B] [Loaded C] Partial output from executing Test with verification off: [Loaded Test] [Loaded A] return to verification return to top Manifestations of the verifier: classes loaded - loader exception if class not found 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. 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 with B.class and C.class removed: [Loaded Test] [Loaded A] java.lang.NoClassDefFoundError: B Partial output from executing Test with verification off, B.class and C.class (or just C.class ) removed: [Loaded Test] [Loaded A] return to verification return to top Manifestations of the verifier: classes are loaded only once 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. class A{ void m1(){} } class B extends A{ } class C extends A{ } class E{ void m2(){ new B().m1(); } } class F{ void m3(){ new C().m1(); } } class Test{ public static void main(String[] args){ System.out.println(0); E a = new E(); // * System.out.println(1); F f = new F(); // ** f.m3(); System.out.println(2); } } Partial output from executing Test with verification on: [Loaded Test] 0 [Loaded E] [Loaded A] [Loaded B] 1 [Loaded F] [Loaded C] 2 return to verification return to top Verification of a class requires verification of its superclasses 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. class A{ void m1(){} } class B extends A{ } class C extends A{ } class E{ void m2(){ new B().m1(); } } class F extends E { void m3(){ new C().m1(); } } class Test{ public static void main(String[] args){ System.out.println(0); F a = new F(); // * System.out.println(1); } } 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 return to verification return to top Verification of a class may load further classes without requiring their verification 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] return to verification return to top Verifier checks subtypes for field access 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] return to verification return to top Verifier checks subtypes for receiver in method calls 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] return to verification return to top Verifier checks subtypes for arguments of method calls 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] return to verification return to top Verifier does not check subtypes for assignments 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] return to verification return to top Verifier does not check existence of types 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 . class A{ int i ; void m(){} } class Test{ void m ( ){ A a = new A(); int j = a.i; // * a.m(); // ** } public static void main(String[] args){} } Partial output from executing Test with verification on (or off): [Loaded Test] return to verification return to top Verifier does not check whether a class implements an interface 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] [Loaded I] [Loaded A] 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: [Loaded Test] [Loaded I] [Loaded AA] [Loaded A] return to verification return to top VerifyError , and the effect of bypassing verification 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. return to verification return to top Preparation Preparation 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. return to top Resolution Binary 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: a field or method of a given type declared in a certain type and that type does not contain such a field or method, a method from an interface of a certain name, but this name belongs to a class, a method or field from a class of a certain name but the name belongs to an interface. Failure of resolution usually causes an IncompatibleClassChangeError or a subclass, such as one of the following: InstantiationError change of class to an interface change of an interface to a class change of class to an abstract class NoSuchFieldError NoSuchMethodError IllegalAccessError - caught only if the verifier has been previously run UnsatisfiedLinkError return to top InstantiationError - change of class to an interface 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 return to resolution return to top InstantiationError -- change of an interface to a class 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 return to resolution return to top InstantiationError - change of class to an abstract 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 return to resolution return to top NoSuchFieldError 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 . return to resolution return to top NoSuchMethodError 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 return to resolution return to top IllegalAccessError 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. return to resolution return to top UnsatisfiedLinkError There is a reference to a method written in another language and this method is no longer available. return to resolution return to top Delegating loader 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. //based on delegating loaders written by Saraswat and Coglio import java.util.*; import java.io.*; class DelegatingLoader extends ClassLoader{ private String loaderId, dir; private Set classIds; private DelegatingLoader nextLoader; public DelegatingLoader (String id, String dir, Set classNames, DelegatingLoader next){ this.loaderId = id; this.dir = dir; this.classIds = classNames; this.nextLoader = next; } protected Class loadClassFromFile(String name)throws ClassNotFoundException, FileNotFoundException{ File target = new File(dir + name.replace('.','/') + ".class" ); if(! target.exists()) throw new FileNotFoundException(); long bytecount = target.length(); byte [] buffer = new byte[(int) bytecount]; try{ FileInputStream f = new FileInputStream(target); int readCount = f.read(buffer); f.close(); Class c = defineClass(name,buffer,0,(int) bytecount); System.out.println( "load <" + name + ", " + loaderId + "> from current-directory/" + dir); return c; } catch (Exception e){ System.out.println("Aborting read: " + e.toString() + " in + LocalClassLoader." ); throw new ClassNotFoundException(); }; } public Class loadClass(String name) throws ClassNotFoundException{ try{ Class prevLoaded = this.findLoadedClass(name ); if (prevLoaded != null) return prevLoaded; else if (classIds.contains(name)){ System.out.println("defining loader <" + name + ", " + loaderId + ">"); return this.loadClassFromFile(name); } else if (nextLoader != null && nextLoader.classIds.contains(name)){ System.out.println("delegating to <" + name + ", " + nextLoader.loaderId + ">"); return nextLoader.loadClassFromFile(name);} else System.out.println("delegated to <" + name + ", system loader>"); return this.findSystemClass(name); } catch (Exception d){ System.out.println("Exception " + d.toString() + " while loading " + name + " in DelegatingLoader."); throw new ClassNotFoundException(); } } } return to loading return to top Program whose classes are loaded by different loaders 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 import java.util.*; public class Test{ public static void start(){ new A().m(); System.out.println("******** after executing new A().m();"); new C().m(); System.out.println("******** after executing new C().m();"); } public static void main(String[] args){ try{ String l1 = "loader1"; String l2 = "loader2"; HashSet names1 = new HashSet(); names1.add( "A"); names1.add( "Test"); HashSet names2 = new HashSet(); names2.add( "B"); DelegatingLoader loader2 = new DelegatingLoader(l2, "dir/", names2, null); DelegatingLoader loader1 = newDelegatingLoader(l1 , "", names1, loader2); Class t = loader1.loadClass("Test"); Object [] arg = {}; Class [] argClass = {}; t.getMethod( "start", argClass).invoke(null,arg); System.out.println( "******** after executing start()"); } catch (Exception e){ System.out.println( "Error " + e.toString() + " in Test.main." ); e.printStackTrace(); } } } ///////////////////////////////////////////////////////////////// public class A extends B{} ///////////////////////////////////////////////////////////////// public class C{ public void m(){ System.out.println(2); } } //////////////////////// from subdirectory dir//////////////////// public class B{ public void m(){ System.out.println(1); } } ///////////////////////////////////////////////////////////////// output from executing Test with verification on and verbose off: defining loader delegated to load from current-directory/ delegating to delegated to load from current-directory/dir/ defining loader load from current-directory/ delegated to delegated to delegated to delegated to delegated to delegated to 1 delegated to delegated to ******** after executing new A().m(); delegated to 2 ******** after executing new C().m(); ******** after executing start() Process Exit... return to loading return to top Program which fails its loading constraints 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 import java.util.*; public class Test{ public static void start(){ A a = new A(); System.out.println( "******** after executing A a = new A();"); a.m(); System.out.println( "******** after executing a.m();"); B b = new B(); System.out.println( "******** after executing B b = new B();"); b.m(); System.out.println( "******** after executing b.m();"); } public static void main(String[] args){ try{ String l1 = "loader1"; String l2 = "loader2"; HashSet names1 = new HashSet(); names1.add( "A"); names1.add( "Test"); HashSet names2 = new HashSet(); names2.add( "B"); names2.add( "A"); DelegatingLoader loader2 = new DelegatingLoader(l2, "dir/" , names2, null); DelegatingLoader loader1 = new DelegatingLoader(l1, "",names1, loader2); Class t = loader1.loadClass("Test"); Object [] arg = {}; Class [] argClass = {}; t.getMethod( "start", argClass).invoke(null,arg); System.out.println( "******** after executing start()"); } catch (Exception e){ System.out.println( "Error " + e.toString() + " in Test.main." ); e.printStackTrace(); } } } ///////////////////////////////////////////////////////////////// public class A{ public A m(){ System.out.println(2); return new A(); } } //////////////////////// from subdirectory dir//////////////////// public class A{ public A m(){ System.out.println(0); return new A(); } } //////////////////////// from subdirectory dir//////////////////// public class B{ public A m(){ System.out.println(1); return new A(); } } ///////////////////////////////////////////////////////////////// partial output from executing Testwith verification on and verbose off: defining loader delegated to load from current-directory/ delegated to delegated to delegated to delegated to defining loader load from current-directory/ delegated to delegated to ******** after executing A a = new A(); 2 ******** after executing a.m(); delegating to delegated to load from current-directory/dir/ ******** after executing B b = new B(); delegated to delegated to 1 defining loader Error java.lang.reflect.InvocationTargetException in Test.main. java.lang.reflect.InvocationTargetException: java.lang.LinkageError: Class A violates loader constraints return to loading return to top A larger example, putting it all together 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(), followed by new D() , followed by D.m1() , followed by new A() , followed by A.m2() 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 imposed by the dynamic linking process, i.e. 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 verify D prepare 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 verify A load C verify B verify C load D verify D prepare Test prepare A prepare B prepare C prepare D resolve Test.main() evaluate Test.main() evaluate new D() resolve D.m1() evaluate D.m1() evaluate new A() resolve A.m2() evaluate A.m2() return to top Verification Preparation Resolution Loading References 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. --------------070208090901070905000105--