Support custom class loaders in AOT cache

ioi.lam at oracle.com ioi.lam at oracle.com
Wed Jan 28 22:00:42 UTC 2026


We have been brainstorming about supporting custom class loaders in the 
AOT cache. While the design is far from final, in our small group 
discussions, we seem to be converging on this:

- Only custom class loaders that are known to produce *stable* results 
can store classes in the AOT cache
- Stable result is roughly: when given a class name X, 
loader.loadClass(X) will always return a class with the same shape
- Also, loader.loadClass(X) should not produce any observable side 
effects, other than the fact that class X has been loaded. E.g., don't 
set any static fields inside loadClass:
- It's completely up to the custom class loader to decide whether it 
meets the AOT cache requirement.

Some examples:
- A URLClassLoader that loads from a fixed set of JAR files in the local 
file system that are known to never change
- A code generator that always generates the same code shape given the 
same class name

A counter example:
- A code generator that mixes code with a random seed

The handshake between the class loader and the AOT cache might look like 
this:

     URL[]  urls = new URL[] {"foo.jar", "bar.jar"};
     URLClassLoader loader = new URLClassLoader(urls);
     String UID = "URLClassLoader$foo.jar:" + cksum("foo.jar") + 
"$bar.jar:" + cksum("bar.jar");
     loader.setAOTCompatible(UID);
     loader.loadClass("com.foo.Foo");
     loader.loadClass("com.bar.Bar");

In the training run, the JVM will store all classes loaded by this 
loader into the AOT cache. These classes are tagged with the given UID.

In the production run, when setAOTCompatible(UID) is called, the JVM 
checks if the AOT cache has any classes tagged with the UID. If so, 
these classes are automatically loaded into the loader *without any 
observable side effect*. Note that the usual handshake of 
ClassLoader::{findClass, loadClass, defineClass}, etc, does not happen. 
The classes simply appeared in the loader out of thin air.

The UID provides a way for the loader to identify itself, as well as 
encoding the dependencies that were assumed during the training run. In 
the above example, we use the checksum of each JAR file to make sure 
that these files haven't changed (or disappeared).

Note that we don't actually cache the loader object itself. The loader 
object will probably have references to environment states that cannot 
be safely stored into the AOT cache. Also, the creation of the loader 
during the training might produce side effects that cannot be easily 
captured into the AOT cache.

We will likely have some restrictions on the behavior of the "AOT 
compatible" loaders

- loader.setAOTCompatible() must be called before any class is defined 
in this loader. Otherwise setAOTCompatible() will throw an 
IllegalStateException
- Only classes with simple ProtectionDomains will be stored into the AOT 
cache. For example, if the loader defines a class with a 
ProtectionDomain that uses a signed code source, the class will be 
excluded from the cache.

Some implementation details:

Ashutosh is working on a prototype. I think we can store the classes 
into the AOT configuration file at the end of the training run:

- Store the Java mirror of the class into the AOT configuration file 
(this requires https://github.com/openjdk/jdk/pull/29472 )
- Also save the ProtectionDomain in the mirror

In the assembly phase, load the classes of each UID into an instance of 
jdk.internal.misc.CDS$UnregisteredClassLoader. This way we can handle 
classes of the same name defined in two different UIDs.
If two UIDs have a parent/child relationship, we should recreate that 
with the UnregisteredClassLoader. This is needed for constant pool 
pre-linking..

The above are just my random notes. Please add your thoughts.

Thanks
- Ioi


More information about the leyden-dev mailing list