----------------------------------------------------------- Design and Implementation of Default Methods in Hotspot JVM ----------------------------------------------------------- Keith McGuigan 9/18/2012 1. Background/Motivation 1.1 Overview A default method (sometimes called an "extension" method or "defender" method) is a method in a Java interface which contains executable bytecode, just as a non-abstract method in a normal class. Methods in an interface typically provide only an API which implementing classes must implement. Default methods provide both an API, and an implementation but the implementation is used only in the case where the implementing class does not provide an implementation of its own (i.e., neither it nor any class is subclasses has a corresponding implementation of the method). Default methods are used to extend an interface in such a manner that it does not break existing classes which already implement the interface. Typically if one added a abstract method to an interface, one would have to find all implementing classes and add an implementation for that method, lest they get an AbstactMethodError when invoking it. This is not always possible, as the interface extender may not always have access to the classes which implement it. Take, for example, java.util.Collection. If a JDK library developer wanted to add a new method, int size(), to the Collection interface, he could update all the classes which implement Collection in the JDK library, but he would not have access to application code which might have classes which implement Collection. Those classes would be broken and one could not rely on calling Collection.size() in the library because of this. Instead, the developer can add Collection.size() as a default method and provide a general implementation directly in the Collection interface itself. Any clients then without a size() method would default to Collection's version and library code could rely on calling Collection.size(). Specializations are free to provide their own size() method and if that is available, it will be used instead. The overall goal is to maintain binary compatibility as much as possible when making changes to existing library code. For example, adding an interface method is usually a binary incompatible change, but if the method is added as a default method, then there is no incompatibility introduced. In addition, operations such as replacing a concrete method in a superclass with a default method from an implemented interface should not cause incompatibilities. Of course, were javac to recompile all the code it would create the correct calls sites, but the JVM should support (as much as possible) the correct operation in a separately-compiled environment. This applies also to bridging issues, where a class may contain a method which has a different erased signature than a supertype's method, though the methods are equivalent at the language level where generics are used. Details can be discovered in the linked specifications below, but what to take from this for JVM work is that default methods must be callable from any (perhaps previously-existing) call site, whether it be 'invokevirtual', 'invokespecial', or 'invokeinterface'. Also, the JVM must provide whatever bridges are needed so that calls to a default method using an erased signature will succeed (and perform the appropriate checkcasts) if the methods matches at the language-level (i.e., generics). 1.2 Specification and inheritance rules Formal specification of default method's is given in the paper, _Featherweight_Defenders_, available here: http://cr.openjdk.java.net/~briangoetz/lambda/featherweight-defenders.pdf The overall description of extension methods is described in this whitepaper: http://cr.openjdk.java.net/~briangoetz/lambda/Defender%20Methods%20v4.pdf and in the "State of the Lambda": http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-4.html And the entirety of this work is bundled as part of the lambda project in JSR-335: http://jcp.org/aboutJava/communityprocess/edr/jsr335/index2.html Community page: http://openjdk.java.net/projects/lambda/ Another important reference that comes in handy is the Java Virtual Machine Specification's sections on Signatures, which describes how generic signatures are represented in the classfile: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.4 and http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.9 Keep in mind that some of these are working documents may not be completed and may change as the specification is not finalized. [1] One important thing to glean from these references is an understanding of how default methods are selected. Because default methods reside in interfaces, and interfaces do not have the constraint of being single-inheritance, it is possible to inherit multiple conflicting default methods, or even inherit the same default method from many different inheritance paths. There is an algorithm to select which method to use in the case that a concrete class does not provide an implementation. Informally, the algorithm works as follows: (1) If there is a adequate implementation in the class itself or in a superclass (not an interface), then that implementation should be used (i.e., class methods always "win"). (2) Failing that, create the set of methods consisting of all methods in the type hierarchy which satisfy the slot to be filled, where in this case 'satisfy' means that the methods have the same name, the same language-level representation of the parameters, and covariant return values (more on this later). Both default methods and abstract methods will be part of this set. (3) Remove from this set, any method which has a "more specific" version anywhere in the hierarchy. That is, if C implements I,J and I extends J, then if both I and J have a suitable methods, J's method is eliminated from the set since I is a subtype of J -- there exist a more specific method than J's method, so that is eliminated. (4) If the remaining set contains only a single entry, then that method is selected. Note that the method may be abstract, in which case an AbstractMethodError is thrown when/if the method is called. If there are multiple entries in the set, or no entries, then this also results in an AbstractMethodError when called. In addition to having default methods act like regular concrete methods through inheritance, default methods can also be invoked from subclasses using an invoke-super-default construct. Invoke-super-default is used by a subclass to defer to a default method implementation or to disambiguate between conflicting inherited default methods. Invoke-super-default appears in the source code as ".super.()". It is compiled into an invokespecial instruction whose target is ., where the interface is a direct supertype of the current class (the current class must directly implement ). Instead of specifying that the exact named method is to be called, an invoke-super-default construct instead indicates that an analysis should occur and the method to be called is a result of finding the "most specific" default method in the subtree of the inheritance graph, starting at the named interface. Once that method is found (if there exists a unique non-abstract solution), then it is further verified that the resulting method is a method that would have been in the set of applicable methods calculated from the current class, as described above. The importance of this second step is to ensure that one cannot call a default method if there exists anywhere in the current hierarchy, a method which is "more specific" than the resulting method. This is similar to the linking rule that exists that super invocations can only call their direct superclass's methods and that it cannot "jump" over interleaving classes to call some ancestor's method. 1.3 Examples ** Simple example: interface I { int m() default { return 99; } } class C implements I {} From here, any invokevirtual, or invokespecial of C.m()I or an invokeinterface I.m()I with an receiver of C, should succeed and return a value of '99'. ** Erasure example 1: interface I { int m(T t) default { return 99; } } class C implements I {} Similarly, an invocation of C.m(String)I OR C.m(Object)I should succeed here and return '99', regardless of instruction, as should invokeinterface I.m(Object)I. ** Erasure example 2: interface I { int m(T t) default { return 99; } } class C implements I { public int m(String s) { return 11; } } Because of the "class always wins" principal, the method that returns 11 is the only one that will link, 99 never will. In this example the JVM would need to add a bridge of m(Object)I which invokes C.m(String)I. ** Conflict example: interface I { int m() default { return 99; } } interface J { int m() default { return 88; } } class C implements I,J {} As written, any attempt to invoke C.m()I will fail with an AbstractMethodError, as there is no unique default method to call. invokeinterface of I.m()I or J.m()I with an instance of C as a receiver would also fail. Converting I.m()I or J.m()I to an abstract method would not help at all, as there will still be a conflict between an abstract method and a default method (default methods don't automatically win over abstract methods). Adding a method m()I to C, or any superclass of C would resolve the situation and C.m() would be called. The implementation of C.m()I could choose which implementation to use by using the invoke-super-default construct: class C implements I,J { public int m() { return J.super.m(); } } This is valid: C's m()I method would be the implementation, and its execution would invoke J's m() method and return 88. ** Disqualifying example: interface I { int m() default { return 99; } } interface J { int m() default { return 88; } } interface K extends I,J { int m() default { return 77; } } class C implements I,J,K {} This example is the same as above except that now K provides a method that is "more specific" than I's or J's, as K extends both I and J. Note that if K did not extend J (for instance), then C would have two choices for m()I: K.m()I and J.m()I, and this there would still be a conflict (and an AME). ** Class-always-wins example: interface I { int m() default { return 99; } } class A { public int m() default { return 11; } } class B extends A {} class C extends B implements I {} In this case calling C.m()I will always return 11. C's superclass chain provides an implementation so that will be the one that's used. 2. Design 2.1 Assumptions and Goals The main assumption of this design is that default methods will be rather rare. Out of all the classes loaded, those that need default method implementations will be a small percentage of the classes. One of the goals of the design is to implement default methods in a way as to reduce the implementation impact upon the rest of the JVM. For instance we do not want to make any major changes to the method data structures, or linkage logic at this time, as there would likely be a lot of follow-on work that would be necessary in reflection, compiler, serviceability, etc. A guiding principal that this design is based upon is that the cost for implementing default methods should be borne only by the classes that use default methods or have default methods in their inheritance hierarchy. As we're assuming that default methods are rare, this ought to leave the performance characteristics of JVM relatively unscathed. If there are performance issues due to default methods, the Java developer has the easy solution of modifying his code to use less default methods. Furthermore, when it comes to making a trade-off between memory footprint and performance, we'd rather not increase the memory resident size. There has been a lot of work lately by the embedded team to reduce the runtime footprint and if possible we'd rather not add additional data to our structures and lose the gains that were made in these areas. [2] In fact, the current implementation seems to do a rather good job of attaining these goals. The cost of default methods is borne only by concrete classes which need default methods for their implementation, and the one-time cost consists only of temporary memory usage at class-loading time. Invoke-super-default incurs this cost during the method's linking stage (a one-time cost). 2.2 Method-injection The implemented solution performs analysis upon classes at class-loading time and performs method-injection to implement default method inheritance. New methods are created and added to the class which vector to the default's implementation. These generated methods do nothing but load the arguments (performing type-checks if necessary), and then performs an invokespecial instruction with the selected interface method as the target, and return the result (type-checked), if necessary. In the code, these methods are referred to as 'overpasses', because they are similar to, but different than a bridge (which is a javac-generated method). This has a number of advantages. First off, the injected methods are added during class parsing and so appear to the rest of the JVM as normal methods. In fact, they are normal methods completely except for the fact that they have a bit identifying them as special and they contain code which performs the "weird" operation of invokespecial on an interface method. Because the methods are injected so early in the class loading process and added to the class's method array, they are incorporated into the virtual method and interface method tables without any modification to that code. As such they are callable using any invoke instruction and there is no special-case code for reflection or compilation. Given the simplicity of of generated method, is is very likely that the compiler will be able to inline or even eliminate this extra frame. For the invoke-super-default case, the default method analysis occurs when the callsite is linked (usually upon first invocation). Once linked the analysis need not necessarily be done again, but the compiler may end up calling the resolution code again during compilation (without problem, as the result is idempotent). 3. Implementation 3.1 Generic types Unlike usual typing in the JVM, default methods require a different definition of equivalency. Whereas overriding rules and method lookup typically examines only the method's name and erased signatures, the definition of "equivalent" in the context of default methods means that the methods have the same name, the language-level signature for the parameters, and a covariant return type. In order to determine if methods are equivalent at the language-level, the JVM must examine and parse the "Signature" attribute of both classes and methods, and resolve the types of a signature based upon the context of the holder class. For example, given the code: class D { public void m(U u) {} } class C extends D { public void m(String s) {} } The signature of D.m() is "(Ljava/lang/Object;)V", while C.m()'s signature is "(Ljava/lang/String;)V". The erased signatures are different because of erasure but at the language-level, these methods have the same signature. To discover this in the JVM, we need to look at the contents of the Signature attribute which contains the generic information we can use to discover this equivalency. Here is the contents of the generic signatures for each class and method: class D: "Ljava/lang/Object;" D.m: "(TU;)V" class C: "LD;" C.m: "(Ljava/lang/String;)V" What to note here is that D has a formal parameter list for the generic types, which in this case is just 'U' (it also contains supertype information which isn't interesting in this case). A generic method signature is very similar to an normal signature, except that it supports a new basic type, 'T' (called a type variable) which references a formal type parameter. If there are no type variables in the method's signature, then the type signature and the runtime signature are identical (runtime signatures are a subset of method generic signatures). The type variable can reference a formal parameter in the method itself [3], or the containing class or its outer class (in the case of inner/outer classes). In this case D.m references the U which was declared D's generic signature. The generic signature for C also contains a type argument attached to its superclass definition. In this case, the type is a type argument is java/lang/String and it matches up with the formal type parameter U declared in D. When D is viewed from the context of C, U == java/lang/String, and thus D.m()'s effective signature (in this context) is "(Ljava/lang/String;)V". This is how language equivalency is determined. The code to parse and process these generic signatures is in the new files. vm/shared/classfile/genericSignatures.[ch]pp. The entirety of the class or method generic signature is contained in the *Descriptor classes, and there are classes to represent type parameters (and their bounds), type variables, and type arguments as well classes, arrays and primitive types. After being parsed, the type description is stored in a parse tree and type formals and type variables are linked up ordinally (by link_variables()) for fast resolution when type arguments are available. These parse trees are allocated in resource memory and stored in the generic::DescriptorCache object during processing. The cache is an InstanceKlass (or Method*) map to an already-parsed descriptor. We do this because we may encounter the same type (or method) multiple times when performing default method processing. If the requested descriptor is not present in the cache, it is parsed at this time. When the default method processing of a class is complete the entire cache is discarded (along with the parse trees) as part of the resource memory evacuation. Whenever we examine signatures we must do so from the context of some root class -- typically the class that's being analyzed for default methods. As we iterate over the class's type hierarchy, we accumulate the type arguments along the inheritance path into a generic::Context instance. As we move up and down the hierarchy, we push or pop the type arguments appropriate for the particular edge in the Context. This Context is used to specialize method descriptors (canonicalize()) turn them into runtime signatures (reify_signature()), or to compare descriptors to each other (covariant_match()). Note that covariant_match() ignores the return value, assuming that if the parameters match then the return type is at least covariant (i.e., assignable). If not, this will be caught at runtime by a checkcast. A quick example of maintaining the context during hierarchy iteration: interface J { void m(U u); } // m: "(TU;)V" interface I { void m(W w); } // m: "(TW;)V" interface K extends J, I {} // "L:J:I; class C implements K {} // "java/lang/Object:K When iterating over C's hierachy: C up K: ctx:{} push X=Integer K up J: ctx:{X=Integer} push U=X J.m(): ctx:{X=Integer,U=X} resolve "(TU;)V" => "(TX;)V" => "(LInteger;)V" J down K: ctx:{X=Integer,U=X} pop K up I: ctx:{X=Integer} push W=String I.m(): ctx:{X=Integer,W=String} resolve "(TW;V)" => "(LString;)V" I down K: ctx:{X=Integer,W=String} pop K down C: ctx:{X=Integer} pop end: ctx:{} 3.2 Selecting classes and methods for analysis To reduce the startup cost, we really only want to analyze classes that might need default method processing and leave the rest alone. In order to do this, a bit has been added to InstanceKlass which indicates if there are any default methods in the hierarchy for a particular class. This bit is inherited from supertypes (if the superclass or any implemented interface has the bit set, the current class has it set), or in the case of interfaces, the presence of a default method (realized by seeing a non-abstract method) sets the bit. Only classes which have this bit set are considered for default method analysis, and then only if they directly implement any interfaces. If a class has the default method bit set but does not implement any interfaces, then the processing performed on behalf of the superclass is adequate and the current class will inherit the correct overpass methods. Just because a class has default methods somewhere in its hierarchy does not necessarily mean that it need default method implementation -- only methods which are present in interfaces but not in the class hierarchy are candidates for default method inheritance. Luckily the JVM already discovers empty slots like this when it looks for 'miranda' methods when calculating the size of the virtual method table. The default method analysis code piggybacks upon this mechanism and modifies it to return the array of mirandas. These miranda methods indicate methods that are present in an interface but are not present in the current class. These are referred to as "open slots" and are represented in the code by a method, but the real definition of an open slot is a name/signature pair for which there is no method in the concrete class (or its supertypes). A miranda method is an obvious open slot, but in addition, any overpasses that were created for superclasses are also open slots if the current class does not implement the method. This is because those slots would have been mirandas except for the fact that default method analysis and method-injection happened for the superclass at the time it was loaded. Because the current class implements new interfaces, the analysis that was performed upon the superclass is not valid for the current class, so those overpasses are discarded for the current class, and a new analysis of that slot is performed. 3.3 Performing default analysis Default method analysis is performed for each open slot in the class. The point of default method analysis is to find a set of candidate default methods present in the hierarchy that match the open slot's name and language-level signature. Because we don't initially know where in the hierarchy that the open slot came from, at the beginning of analysis we don't know what the language-level signature is (as we only have the erased signature -- we do know the method descriptor, but we don't have a context to resolve type variables). The analysis iterates over the hierarchy (maintaining generic signature context this time) finding all abstract and default methods that match the slot's name, and collects the discovered methods into disjoint sets based upon the language-level signature. These sets of methods are represented by a MethodFamily instance. Because a single name may be associated with multiple language-level signatures, a list of MethodFamily instances may result. The MethodFamily which contains a method whose erased signature matches the the open slot is the set which is attached to the open slot. Other open slots are also examined to see if one of the discovered MethodFamily instances matches any of other slots, so that the same iteration is not performed multiple times. In addition to collecting candidate methods in a MethodFamily, the state of each method is recorded as well. The state is either "qualified" or "disqualified". A method state is entered (or changed) to disqualified if it is ever found further up in the hierarchy than an equivalent method. After the hierarchy walk is complete, only qualified methods are considered as candidates for the implementation. During the iteration of the hierarchy, if a method that implements the MethodFamily is found in a superclass (not an interface), then that method ends up being the selected implementation rather than any default method (class always wins). In this case, we need only to generate a traditional bridge method from each open slot signature to the concrete method. Assuming there is not a concrete implementation for the slot, the set of qualified methods is examined to see if there is a valid default method for this slot. If there is a single qualified default method in the set, then that method is used as the implementation for the set, and an overpass method is generated which uses an invokespecial instruction to call that method. Otherwise, if there are no qualified methods, or multiple qualified methods, or a single abstract method, then we still generate an overpass method for the slot, but in this case the overpass creates and throws an AbstractMethodError with the appropriate error message. In all cases, when we generate an overpass method for a particular slot, we also create overpass methods for every erased signature that we find in the equivilence class, no matter their origin or state. This ensures that a call site targeting any of these signatures will match a method. Any such bridges that we create that doesn't which exactly match the target's signature will insert checkcast instructions at the appropriate place when loading differently-type arguments or return value. 3.4 The Hierarchy Visitor The core of the analysis (and also used by the implementation of invoke-super-default) is the HierarchyVisitor class. The HierarchyVisitor class implements iteration over a class's hierarchy (a DAG) in a depth-first fashion, possibly visiting some interfaces more than once. The visitor intentionally does not perform the iteration recursively, as we do not want to push the limits of the stack as we could overflow with deep hierarchies. Instead resource memory is used to store the current path and the state of the iteration. The HierarchyVisitor is responsible only for moving over the hierarchy -- the actual algorithm is coded in a separate class using the "Curiously recurring template pattern" [4], to avoid virtual calls. The algorithm class has callbacks which are called by the visitor as it iterates over the tree. The comments in the code describe this setup pretty well. Refer to PrintHierarchy for a simple example of using this visitor model. FindMethodsByName is the algorithm that is paired with the HierarchyVisitor to implement the default method collection described in the previous section (generating MethodFamily instances). 3.5 Creating and merging overpass methods The files bytecodeAssembler.[ch]pp contain the code for assembling bytecode into a buffer (a GrowableArray). The assembler itself is only a partial assembler and supports only the bytecodes that we need when creating overpass methods. It could easily be augmented with additional opcodes/operands to become a more general bytecode assembler. The BytecodeConstantPool class is used in conjunction with the BytecodeAssembler to hold constant pool entries that will be added to the class's constant pool. It is initialized with the existing constant pool, mostly just to know where to start with its indicies (the length of the original constant pool). Whenever the bytecode assembler needs to refer to a constant pool entry (such as a Symbol or a MethodReference), the BytecodeConstantPool returns an index to that entry, creating a new entry if needed. This index is then used as an operand to the bytecode. Current the BytecodeConstantPool does not scan the existing constant pool to make a mapping for entries that already exist -- so it's possible that the the entries created by the BytecodeConstantPool will duplicate entries in the original constant pool. This is harmless, though not very space efficient. A single BytecodeConstantPool can be (and is) used for all overpass methods. Once all of the methods for a class have been generated, there is code in BytecodeConstantPool to create a new ConstantPool object and copy the data from the old constant pool into it along with the new entries creating during bytecode assembly. This new constant pool is then installed as the class's constant pool and the old CP and discarded/deallocated. Because we require the class's generic signature attribute to be parsed before we can do the default method analysis, we must delay analysis until after the entire classfile has been read. By this time in the normal class file parsing process, the methods have already been parsed, placed into a fixed-size array, and sorted. In order to inject methods, we need to reallocate that array to make room for the new overpass methods, and place them into the new array in sorted order. To do this we first sort the array of overpasses, and then merge the old method array with the new overpass methods into the newly allocated method array. During the merging process, we need to take care to merge the annotations arrays in the same order as the methods, as they correspond via array index. The new arrays are installed into the InstanceKlass and the old ones are discarded. 3.6 Invoke super default The invoke-super-default logic is invoked at link-time when we encounter a call-site that uses invokespecial that references a method in the current class's directly implemented interface. This is the signal that the call site is an invoke-super-default. To determine the method to call, FindMethodsByName is again used, but this time using the specified interface as the root. If the resulting set of candidates is not a unique default method, an AbstractMethodError is immediately thrown. Otherwise, if there is a unique default method, there is one additional check to do, which is to confirm that the selected default method would be in the set of default method candidates if the algorithm was run on the current class. We could certainly do that again, but there is a more efficient way to figure this out. Since the selected method is a candidate from the specified interface, the only way it would not be in the candidate set for the current class would be if there exists a path in the hierarchy from the current class to the specified interface and along that path there is an equivalent method. The existence of an equivalent method along that path would indicate that the selected method would end up getting a disqualified state when running the full algorithm on the current class. So instead of performing a full analysis, the ShadowChecker class used. The ShadowChecker is another example of the "algorithm" part of HierarchyVisitor. ShadowChecker starts at the current class and looks for non-direct paths to the specified interface. If any such paths exist, the checker verifies that no equivalent method exists along that path. If any does, then the selected method is invalid (as it is demonstratively not the "most specific" method) and a AbstractMethodError is thrown. If the ShadowChecker does not find any methods that shadow the selected method, then that method is linked to the call site and the process is complete. This is unfortunately an ambiguity between the invoke-super-default pattern, and the bytecode pattern we use when creating the overpass methods. If the class inherits a default method from an immediate interface, then it will contain an invokespecial instruction that refers to an immediate interface's method. This looks exactly like what one expects from a invoke-super-default, and in fact at link-time this logic will be triggered. Luckily in this situation the logic will find the right method and linking will be fine, but we do end up doing generic analysis twice for these cases. This could be avoided by recognizing that a invoke-super-default pattern does not appear inside overpass methods, so if we detect that the invoking method is an overpass we could skip this processing. This is an optimization that may be added later. 3.7 Miscellaneous Because interfaces now have bytecode in them, we must initialize them at the time that an implementing class is initialized. Verification for overpass methods can be skipped since the JVM generates the bytecode and we can be assured that it's valid. The code add another back-door to bypassing access control (but not verification) through java.lang.invoke.MagicLambdaImpl, which is used by javac to implement lambdas. This is actually a fully-separate feature from default methods but is present in the lambda hotspot repositories. 4. Footnotes [1] Any of the requirements not explicitly spelled out in the documents were obtained through conversations with Brian Goetz and Robert Field who are both active on the lambda project (and to whom I am very grateful!). Any clarifications or questions about requirement origins should be direct to them. [2] We can certainly make some different decisions based upon different design priorities. If startup time is more important than footprint, for instance, we could investigate caching the generic signature type data structure with each class. Right now this is used once and then thrown away, only to be recreated if another subclass needs to examine it. We could also store some meta-information in the methods which indicate which methods in super or subclasses which it is equivalent to, and possible shadows. [3] We have no use for method formal type parameters in default method processing, so we ignore them in the implementation and erase them to their lower bound. [4] http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern