Class ClassLoadingChainAnalyzer

java.lang.Object
io.quarkus.deployment.pkg.steps.ClassLoadingChainAnalyzer

class ClassLoadingChainAnalyzer extends Object
Discovers classes that are loaded dynamically via 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.RecordingClassLoader
extends URLClassLoader 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 Details

    • ClassLoadingChainAnalyzer

      ClassLoadingChainAnalyzer(Map<MethodKey,Set<MethodKey>> callerIndex, Set<String> depClassNames)
      Parameters:
      callerIndex - pre-built reverse call index (callee → callers), populated during BFS
      depClassNames - dependency class names (dot-separated) for entry point filtering
  • Method Details

    • findEntryPoints

      Set<String> 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.