Class Loader API Modifications for Deadlock Fix
-
Version .04: March 30, 2009
-
Authors: Valerie Peng, Jeff Nisewanger, Karen Kinnear
-
Audience: Community Review
Goals
-
The goal of Class Loader API modifications is to resolve a critical customer issue: it is easy to deadlock custom class loaders that do not adhere to an acyclic class loader delegation model.
Non-Goals
-
Other Class Loader RFEs that require API modification will be addressed via individual RFEs, separately from this fix for the deadlock problem.
-
This proposal does not address any re-architecture for performance
Motivation
-
Top webbug: 817 SDN votes April 2008 4670071 java.lang.ClassLoader.loadClassInternal(String) is too restrictive
-
Causing problems for key customers with no good workaround that works for everyone.
Technical Design Constraints
-
Must allow non-hierarchical class delegation topologies without deadlocks
-
Must be backwards compatible
-
Existing class loaders must continue to work unchanged
-
including class loaders which override findClass(...), loadClass(String), and/or loadClass(String, boolean)
-
Existing code that invokes a class loader directly via loadClass(String) or a class loader that calls loadClass(String, boolean) must continue to work unchanged
-
-
Must continue to support the temporary risky flag combination -XX:+UnlockDiagnosticVMOptions -XX:+UnsyncloadClass
-
The intention is to deprecate the -XX:+UnsyncloadClass flag when customers adopt the new mechanism.
-
-
Common workaround
-
Right now some customers work around the deadlock by explicitly issuing a wait() on the class loader lock. While this has not been sufficient to solve many customers problems, we need to continue to support this until those customers have an opportunity to migrate to the new mechanism.
-
-
Must be possible to define guidance to migrate custom class loaders to take advantage of asynchronous behavior
-
Today, with the class loader lock held around class loader operations, class loaders are not required to handle asynchronous multi-threaded class loading. A mechanism which avoids deadlock will require multi-thread safety awareness. Given that supporting multi-threaded class loading requires careful thought and planning, the new mechanism must provide a way for a class loader to explicitly claim that they are multi-thread safe.
-
Therefore: the fix for these bugs will require code changes by custom class loader authors if their class loaders are candidates for deadlock.
-
Given that each class loader that chooses to adopt the new mechanism must be explicitly safe for concurrent class loading by multiple threads, the claim that a class loader is multi-thread safe must not be automatically inherited.
-
Class loaders that adopt the new mechanism must be able to run on older JREs. So, for example, the design must allow for a way to detect if the mechanism exists, and can not count on inheritance of classes or interfaces that may not be present.
-
Goal: minimal code changes for custom class loader authors
-
Goal: If we need to make trade offs on how much thought and changes are needed for custom class loader authors, depending on whether they override findClass(String) or loadClass(String)/loadClass(String, boolean), make it simpler for those who override findClass(String). There are more of those custom class loaders and generally to override either loadClass(...) already requires dealing with more class loader complexity.
Class loader Deadlock Problem
Brief Overview of Class Loading Interactions Between Class loader and the VM
Class loading requires cooperation between the VM and user level class loaders. Specifically, when the VM is performing constant pool resolution, among other things, the VM needs to call out to the user level class loader in order to find and define the requested class. On the other side, the class loader responsible for loading a class needs to call into the VM to determine if the class has already been loaded (findLoadedClass), and to define the class based on a bytecode stream (defineClass).
The current recommended logic in detail is:
User Level Class Loader VM
constant pool resolution
first acquire class loader lock
-->private synchronized loadClassInternal(String) <--- calls out to loadClassInternal(String)
| public loadClass(String)
| protected synchronized loadClass(String,boolean)
| protected final findLoadedClass(...) ---> VM SystemDictionary cache lookup
| (can not trigger further class loading)
| delegate (e.g. parent.loadClass(String))
| protected findClass(String)
| reads in bytes
| protected final defineClass(...) ---> resolves superclasses and superinterfaces
| which recursively calls out to
-------------------------------------------------- <--- loadClassInternal(String)
Custom class loader authors are encouraged to override findClass(String) to allow them to determine where or how the byte code stream is obtained. Some custom class loader authors override loadClass (String) or loadClass(String, boolean) to be able to change the delegation strategy.
Currently many class loading interactions are synchronized on the class loader lock. This works well for class loader delegation that assumes a DAG-based delegation hierarchy.
Customers have requested the ability to delegate to arbitrary class loaders. Currently this can cause deadlocks if class loaders delegate to each other without a fixed ordering.
Sample Deadlock Scenario: non-tree based delegation hierarchy
Class Hierarchy:
class A extends B
class C extends D
ClassLoader Delegation Hierarchy:
Custom Classloader CL1:
directly loads class A
delegates to custom ClassLoader CL2 for class B
Custom Classloader CL2:
directly loads class C
delegates to custom ClassLoader CL1 for class D
Thread 1:
Use CL1 to load class A (locks CL1)
defineClass A triggers
loadClass B (try to lock CL2)
Thread 2:
Use CL2 to load class C (locks CL2)
defineClass C triggers
loadClass D (try to lock CL1)
Proposed Solution
User Level Class Loader VM
constant pool resolution
If ParallelCapable:lock class/class loader pair
else acquire class loader lock
*DEPRECATE*: private synchronized loadClassInternal
|-->public loadClass(String) <--- calls out to loadClass(String)
| call loadClass(String,boolean) (*no longer synchronized*)
| *if ParallelCapable:*
| *synchronize on a class-name-based-lock*
| *else synchronize on "this" (backward compatibility)*
| protected final findLoadedClass(...) ---> VM SystemDictionary cache lookup
| (can not trigger further class loading)
| delegate (e.g. parent.loadClass(String))
| protected findClass(String)
| reads in by
| protected final defineClass(...) ---> resolves superclasses and superinterfaces
| which recursively calls out to
-------------------------------------------------- <--- loadClass(String)
API Modifications
Java APIs
-
New protected static method in java.lang.ClassLoader, boolean registerAsParallelCapable()
-
To be called once from class initialization of a subclass of java.lang.ClassLoader which supports parallel loading of classes.
-
In order for a class loader class to register itself as parallel capable, its superclasses must also already be registered as parallel capable when invoking this protected static method. This is the simplest way to ensure that any methods this class loader inherits have been explicitly made multi-thread safe. Otherwise the request will be ignored.
-
registerAsParallelCapable() will return true if the class loader successfully registers as parallelCapable, even if accidentally called a second time. It will return false on failure.
-
-
New protected method in java.lang.ClassLoader, Object getClassLoadingLock(String className)
-
Returns the lock object for class loading operations.
-
For backward compatibility, the default implementation of this method behaves as follows:
- If this ClassLoader object is registered as parallel capable, the method returns a dedicated object associated with the specified class name.
- Otherwise, the method returns this ClassLoader object.
-
-
java.lang.ClassLoader API changes:
-
Deprecate private loadClassInternal(), retain temporarily for backward compatibility
-
Remove the synchronized keyword from:
-
protected synchronized Class<?> loadClass(String name, boolean resolve) method.
-
public synchronized void setDefaultAssertionStatus(boolean enabled)
-
public synchronized void setPackageAssertionStatus(String packageName, boolean enabled)
-
public synchronized void setClassAssertionStatus(String className, boolean enabled)
-
public synchronized void clearAssertionStatus()
-
-
VM Flags for early access testing
-
AlwaysLockClassLoader - default false. Provided for ParallelClassLoader that has a bug in handling multiple threads in parallel. Expected to ship in product.
-
AllowParallelDefineClass - default false. If two threads try to define the same class/class loader pair in parallel, throw linkageError, duplicate class definition. If you change the flag, we would allow parallel defineClass requests, using the result of the first requester. This would require a JVMS clarification. Experimental in product for early access feedback only, not expected to ship.
-
MustCallLoadClassInternal - default false. In case a customer actually depended on this call, perhaps expecting to find this on the stack. Note that this will only enforce calling loadClassInternal(String). For instances of parallel capable class loaders, neither the VM nor loadClassInternal(String) will acquire the class loader lock. Expected to ship in product for one or two releases until deprecated loadClassInternal(String) can be safely removed.
Java <-> VM interface changes
-
VM calls loadClass(String) NOT loadClassInternal(String) always (note: backward compatibility -XX:+MustCallLoadClassInternal flag override)
-
VM acquires class loader lock only if class loader instance is not a ParallelCapable class
-
java.lang.ClassLoader's instance constructor will look up the class for a new class loader, and if it is registered as parallel capable, set a private instance field, parallelLockMap to non-null for the vm to query.
Class Loader changes required
-
java.lang.ClassLoader:
-
Add itself to the list of parallel-capable class loaders during class initialization
-
Modify the code to ensure thread-safe concurrency
-
Ensure no internal methods synchronize on the class loader object for parallel capable class loaders
-
Ensure all critical sections are safe when executed by multiple threads loading different classes
-
-
protected loadClass(String, boolean) changes:
-
Remove synchronized keyword
-
If "this" is not a parallel capable class loader, synchronize on "this" for backward compatibility
-
else synchronize on a class-name-based-lock
-
The synchronization in protected loadClass(String, boolean) ensures that defineClass(...) will not be called multiple times in parallel for the same class name/class loader pair.
-
-
AssertionStatus related APIs
-
Replace synchronized keyword with internal synchronization logic for:
-
public synchronized void setDefaultAssertionStatus(boolean enabled)
-
public synchronized void setPackageAssertionStatus(String packageName, boolean enabled)
-
public synchronized void setClassAssertionStatus(String className, boolean enabled)
-
public synchronized void clearAssertionStatus()
-
-
Internal synchronization logic:
-
If "this" is not a ParallelCapable class loader, synchronize on "this" for backward compatibility
-
else synchronize on a dedicated internal lock object for all assertion related fields
-
-
-
add new protected static method: registerAsParallelCapable()
-
If all superclasses which are instances of java.lang.ClassLoader succeeded in registering as ParallelCapable, the calling class will also successfully register as parallel capable.
-
-
-
java.security.SecureClassLoader
-
Invoke registerAsParallelCapable() during class initialization
-
No modifications were necessary to ensure thread-safe concurrency
-
-
java.net.URLClassLoader:
-
Invoke registerAsParallelCapable() during class initialization
-
Modify the code to ensure thread-safe concurrency
-
Suggested Model for Custom Class Loaders
-
Custom class loaders that have no history of deadlocks require no changes.
-
Custom class loaders that support a non-hierarchical delegation model and so are candidates for deadlock may choose to adopt the new mechanism. If you adopt this new mechanism, you need to modify all custom class loaders that could interact in a deadlock.
-
Current class loaders frequently rely on the synchronization on the class loader lock provided by the enclosing methods loadClassInternal and protected loadClass(String, boolean). To resolve the current deadlocking on class loader locks, finer grained locking is needed. For class loader classes that successfully register themselves as parallel capable, the java.lang.ClassLoader class will no longer synchronize on the current class loader object when called to load a class. Instead java.lang.ClassLoader uses its own private lock which is unique for each class name. This allows concurrent loading of different classes for the same class loader instance. This locking logic is in the protected loadClass(String, boolean) method and will be passed to custom parallel class loaders through inheritance if not overridden.
-
Recommended modifications for custom class loaders that need to avoid deadlocks:
-
1. REQUIRED: In the class initializer, invoke java.lang.ClassLoader registerAsParallelCapable()
-
This registration indicates that all instances of this class loader class are multi-thread safe for concurrent class loading
-
If the registration succeeds, java.lang.ClassLoader will allow concurrent loading of classes for the same class loader instance
-
-
2. REQUIRED: Modify the code to ensure multi-thread safety
-
Decide upon an internal locking scheme. E.g. java.lang.ClassLoader uses a class-name-based locking scheme.
-
Remove all synchronization on the class loader lock
-
Ensure that critical sections are safe for multiple threads loading different classes.
-
-
3. REQUIRED: Ensure that all class loader classes that this custom class loader extends also invoke registerAsParallelCapable() in the class initializer and ensure they are multi-thread safe for concurrent class loading
-
4.a Class loaders that invoke registerAsParallelCapable(), that override findClass(String), (recommended overriding):
-
No further modifications required
-
-
4.b. Class loaders that invoke registerAsParallelCapable(), that override protected loadClass(String, boolean) or public loadClass(String)
-
We recommend that you override findClass(String) instead, if at all possible
-
To ensure that the protected defineClass(...) method is only called once for the same class name, you need to implement a finer-grained locking scheme. One option would be to adopt the class name based locking mechanism from java.lang.ClassLoader's protected loadClass(String, boolean) method.
-
-
Internal Implementation Details
The most important point to make is that all class loaders that want the deadlock fix, must be cleaned up to ensure that they are multi-thread safe, i.e. that they allow the class loader to load multiple classes at the same time. The basic approach is to remove synchronization on the class loader itself, and provide smaller granularity locking for critical sections. It is critical that there be no synchronization on the class loader lock for parallel capable class loaders. Given that class loading is frequently triggered implicitly, e.g. by newInstance, and given the interactions between the VM and class loaders, acquiring the class loader lock holds the risk of causing a deadlock.
All of the JRE class loaders which customers extend to create custom class loaders need to invoke registerAsParallelCapable(). They all must be made multi-thread safe for concurrent class loading of different class name/class loader pairs, so methods inherited by parallel capable custom class loaders are thread-safe. Sun will need to commit to these changes for customers to count on, and will need to add this to the documentation of these classes. In addition, in some cases the JRE class loaders need to delegate in other than the traditional tree-based hierarchy. These include:
-
java.lang.ClassLoader
-
java.security.SecureClassLoader
-
java.net.URLClassLoader
-
Extension class loader: sun.misc.Launcher$ExtClassloader
-
sun.misc.launcher$AppClassLoader
Other class loaders do not require any changes. These
include:
- Class loaders with alternative mechanisms for preventing
deadlock
-
javax.management.loading.MLet, PrivateMLet: provides non cyclical delegation based on registration order
-
-
Class loaders that customers can not extend
- sun.applet.AppletClassLoader
- sun.plugin.javascript.JSClassLoader
- sun.plugin.security.PluginClassLoader
- sun.plugin2.applet.JNLP2ClassLoader
- sun.plugin2.applet.Applet2ClassLoader
- sun.plugin2.applet.Plugin2ClassLoader
- com.sun.jnlp.JNLPClassLoader
- sun.applet.AppletClassLoader
VM Behavior Changes
Sincere apologies on how long it has taken us to fix this problem. Part of the reason this fix has taken so long is that we first needed to modify the VM in the following ways:
-
Fixed problems with circularity detection (JDK6)
-
Modified the VM common class resolution logic to handle parallel class loading (HotSpot 10, JDK 6u4)
Details of VM handling of specific class loader
cases:
-
Traditional class loaders
-
When using traditional class loaders to load classes, lock class loader object lock, call loadClass(String)
-
-
-XX:+UnlockDiagnosticOptions -XX:+UnsyncloadClass - DEPRECATED
-
no class loader object lock for class loaders, call loadClass(String)
-
allow parallel overall class resolution, parallel superclass loading, and parallel defineClass() calls
-
parallel defineClass() handling: if there are two define class requests at the same time for the same class/class loader pair, the second requester waits for the first requester and returns the first request or's results, instead of throwing a linkageError.
-
-
-
parallelCapable class loaders
-
No class loader object lock for class loaders, call loadClass(String)
-
allow parallel overall class resolution, and parallel superclass loading
-
parallel defineClass requests will throw LinkageErrors (for early access experimentation: see AllowParallelDefineClass flag)
- VM ignores parallelCapable registration for JDK <=
JDK7
-
-
-
bootstrap class loader
-
No global lock, allow parallel overall class resolution
-
wait for parallel superclass loads to complete
-
parallel defineClass requests will throw LinkageErrors
-
-
-
breaking the class loader lock
-
Special handling: for both overall class resolution and superclass loads: first requester completes, others wait for completion
-
Alternatives Considered and Not Chosen
-
Marker interface.
-
Proposal: Provide a marker interface which a class loader can implement to declare that it is a parallel capable class loader.
-
Reasons not chosen:
-
Custom class loaders which extend a parallel capable class loader will automatically become parallel capable through inheritance. This is not desirable since child class loaders may accidentally become parallel capable if any of their ancestor class loaders are changed to be parallel capable.
-
Some class loaders need to be able to run across various versions of JREs. They can't implement a new interface which does not exist in the older JREs.
-
-
-
Annotations
-
Proposal: Use annotations to indicate that a class loader is parallel capable.
-
Reason not chosen:
-
Annotations were explicitly designed to NOT modify VM runtime behavior, only to provide information, i.e. annotations were not intended to be a macro language for the VM. If run on a VM that does not understand an annotation, the behavior should not change.
-
-
-
New loadClassXXX(String) API without a registration mechanism
-
Proposal: Remove the synchronized keyword from the protected method loadClass(String, boolean), and add a new protected method loadClass2(String) which by default acquires the class loader lock before calling loadClass(String, boolean). Class loaders that support parallel loading should override loadClass2(String) to provide their own synchronization mechanism. Both the VM and the public method loadClass(String) call loadClass2(String) for loading classes.
-
Reasons not chosen:
-
The VM would have no way of knowing if the class loader supports parallel class loading or not. For preventing deadlock the VM needs to allow parallel class resolution and parallel superclass loading. For backward compatibility, the VM needs to disallow parallel class resolution and parallel superclass loading.
-
There are multiple paths into class loading today which must continue to work for backward compatibility.
- Custom class loaders that call the protected synchronized method loadClass(String, boolean) must continue to work. Thus, loadClass(String, boolean) would still have to acquire the class loader lock when calling existing (non-parallel-capable) class loaders. Same applies to loadClass(String).
-
-
-
New loadClassXXX(String, boolean) API in addition to registration mechanism
-
Proposal: In addition to a registration mechanism, introduce a new protected loadClassUnsync(String, boolean) which essentially has the same behavior as the protected method loadClass(String, boolean) except that it does not have the synchronized keyword. VM calls the public method loadClass(String) which then dispatches to loadClass(String, boolean) or loadClassUnsync(String, boolean) depending on whether the class loader is parallel capable or not.
-
Reasons not chosen:
-
Strongly considered and in fact prototyped. While we believe this could work technically, we believe the current proposal is the simplest we have so far devised in terms of requiring minimal changes for custom class loaders which need to deal with deadlocks, and no changes for others.
-
Requires both the same changes the current proposal does for custom class loader author and additional changes:
-
All parallel capable class loaders would need to override loadClassUnsync(String, boolean) and provide their own synchronization mechanism, i.e. we would not provide the per-class-name synchronization that we do today
-
-
-
-
Per-instance registerAsParallelCapable():
-
Proposal: Instead of registering as parallel capable at the class level, allow instances of the same class loader to register themselves as parallel capable or not.
-
Reason not chosen. Not needed.
-
The registration is intended to indicate that a class loader implementation is multi-thread safe. That information is meaningful on a per-class bases. If a custom class loader author wishes, they can go back to locking on the class loader object for instances in which they would like to disable the parallel class loading capability in their implementation. By registering as parallelCapable, they have that option. Note that tracking of parallel capable is based on the Class instance of the class loader and not based on the class name. So different class loaders with the same class name will still be tracked independently.
-
-
-
New LinkageError for parallel duplicate requests
-
Proposal: It has been suggested that one way of handing parallel duplicate requests would be to define a new LinkageError subclass passing back the original defined class, rather than the current error of "duplicate". I think the intention of this proposal was that the caller of defineClass() could check if the original class matched the requested class, and if so, do a findLoadedClass() to use the previously defined class.
-
Reason not chosen: A problem with the proposal is that the VM does not actually cache the original unprocessed byte stream, and it would not be worth the overhead to do so.
-
-
Ability to override the requirement that all superclasses must be parallel capable.
-
Proposal: Some custom class loader authors would like to state that a given class loader is parallelCapable and they guarantee that all superclasses are parallel capable even though they are not registered as such.
-
Reasons not chosen:
-
A strong concern is that authors of the superclass would not know that future changes would be expected to support the multi-thread safety that parallel capable implies, and so there would be a strong risk of breaking undocumented, unofficial, backward compatibility.
-
-
-
Use the term Async instead of parallel capable
-
Proposal: use the term Async.
-
Reasons not chosen:
- Async could imply an analogy with asynchronous I/O, i.e. the class loading would take place later - so that is probably not the best alternative.
-
Open Issues
-
Modify JVMS and defineClass(...) behavior
-
Proposal: There is a suggestion to modify defineClass() behavior such that for parallel capable class loaders, if two defineClass(...) requests for the same class/class loader pair occur in parallel, rather than throwing a LinkageError for a duplicate definition, defineClass() would have enforce first-definition-wins-behavior. All defineClass(...) requests would parse the byte stream for ClassFileFormatError. The first requester will complete the definition process, subsequent requesters will wait and use the results of the first request.
-
The prototype will offer a flag, -XX:+AllowParallelDefineClass, so that you can provide feedback based on experimentation.
-
Feedback: so far both defaults have been requested. Looking for additional feedback once the prototype is available.
-