Class ClassLoadingChainAnalyzer
ClassLoader.loadClass() or
Class.forName() through call chains that cannot be statically traced by the
tree-shaker's BFS reachability analysis.
Problem
Many libraries construct class names at runtime using StringBuilder,
invokedynamic StringConcatFactory, or similar mechanisms, and then load
them via ClassLoader.loadClass() or Class.forName() during static
initialization (<clinit>) or constructor execution (<init>). Because
the class name strings are computed dynamically, the tree-shaker's BFS over constant
pool references and method instructions cannot trace which classes will actually be
loaded. Without special handling, these dynamically loaded classes would be incorrectly
removed from the final artifact.
Solution overview
Rather than attempting to reconstruct class name strings from bytecode (which is fragile and incomplete), this analyzer uses a three-phase approach:
Phase 1 -- Seed propagation
Uses a reverse caller index (callee → callers) that is built externally by
JarTreeShaker.scanBytecode(java.lang.String, byte[], boolean, io.quarkus.deployment.pkg.steps.JarTreeShaker.BfsScanResult) during BFS reachability analysis. Starting from seed
methods (ClassLoader.loadClass(String), Class.forName(String),
Class.forName(String, boolean, ClassLoader), and
MethodHandles.Lookup.findClass(String)), propagates backwards through the
caller index using a fixed-point algorithm to identify every application method that
transitively calls a class-loading seed. JDK and infrastructure classes
(java/, javax/, jakarta/, sun/, org/objectweb/)
are excluded from propagation to avoid false positives.
Phase 2 -- Entry point discovery
Scans the class-loading methods identified in Phase 1 for <init> or
<clinit> methods. These represent "entry point" classes whose construction
or static initialization ultimately triggers the dynamic class loading. No additional
graph walk is needed because Phase 1 already computed the full transitive closure.
Phase 3 -- Dynamic execution in a forked JVM
Each entry point class is loaded and instantiated inside a forked JVM process
managed by ForkedJvmEnvironment (which launches ClassLoadingRecorder)
to dynamically capture all class load attempts that occur during its initialization
and construction. Running in a separate process ensures complete isolation of global
JVM state and that Metaspace is reclaimed when the process exits.
RecordingClassLoader
The
invalid reference
ClassLoadingRecorder.RecordingClassLoaderURLClassLoader with
the platform class loader as its parent, isolating it from the application classpath.
It loads dependency and application classes from URLs built from the forked JVM's
classpath. Every loadClass() attempt is recorded, including attempts that fail
with ClassNotFoundException. After instantiation, if the resulting object
implements java.util.Map, the analyzer also extracts String values from
the map entries and checks them against known class names. This handles patterns like
BouncyCastle's addAlgorithm() which stores class names as map values for
deferred loading rather than loading them immediately.
Fixed-point loop
This analyzer is designed to be called from JarTreeShaker in an outer
fixed-point loop. Each invocation returns newly discovered class names. These are fed
back into the BFS reachability analysis, which may mark additional classes as
reachable, and the analysis repeats. The loop terminates when no new classes are
discovered in an iteration.
Example: BouncyCastle
BouncyCastle's BouncyCastleProvider constructor calls setup(), which
calls loadAlgorithms(), which calls loadServiceClass(), which calls
ClassUtil.loadClass(), which ultimately calls ClassLoader.loadClass().
Phase 1 marks all these methods as class-loading methods. Phase 2 walks up to the
BouncyCastleProvider.<init> constructor and identifies it as an entry point.
Phase 3 instantiates it and records every dynamically loaded class. Additionally, each
$Mappings class stores sub-class names via addAlgorithm(key, className),
which populates the provider's internal map. These class names are captured by
inspecting the map values after instantiation.
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionPropagates backwards from class-loading seed methods through the pre-built caller index to find entry point classes whose<init>/<clinit>triggers dynamic class loading.(package private) voidrelease()Releases the class-loading method data to free memory.
-
Constructor Details
-
ClassLoadingChainAnalyzer
- Parameters:
callerIndex- pre-built reverse call index (callee → callers), populated during BFSdepClassNames- dependency class names (dot-separated) for entry point filtering
-
-
Method Details
-
findEntryPoints
Propagates backwards from class-loading seed methods through the pre-built caller index to find entry point classes whose<init>/<clinit>triggers dynamic class loading. Only returns entry points not returned by previous invocations.- Returns:
- newly discovered entry point class names, or empty set if none found
-
release
void release()Releases the class-loading method data to free memory. The caller index is owned externally and cleared by the caller.
-