oracle/graal · error · IllegalStateException

Package %s cannot be imported from multiple loaders

Error message

Package %s cannot be imported from multiple loaders

What it means

Mirroring jdk.internal.loader.Loader.remotePackage: while wiring the module configuration, each remote (parent-layer) package is mapped to the loader that provides it. If the same package would be imported from two different class loaders, the mapping is ambiguous and this IllegalStateException is thrown — JPMS forbids reading the same package from multiple loaders in one configuration.

Source

Thrown at compiler/src/jdk.graal.compiler.hostvmaccess/src/jdk/graal/compiler/hostvmaccess/HostVMAccessClassLoader.java:248

                    for (ModuleDescriptor.Exports e : descriptor.exports()) {
                        if (!e.isQualified()) {
                            remotePackage(remotePackageMap, e.source(), loader);
                        }
                    }
                }
            }
        }

        return Collections.unmodifiableMap(remotePackageMap);
    }

    /**
     * See {@code jdk.internal.loader.Loader#remotePackage}.
     */
    private static void remotePackage(Map<String, ClassLoader> map, String pn, ClassLoader loader) {
        ClassLoader l = map.putIfAbsent(pn, loader);
        if (l != null && l != loader) {
            throw new IllegalStateException("Package " + pn + " cannot be imported from multiple loaders");
        }
    }

    /**
     * See {@code jdk.internal.loader.Loader#findModuleLayer}.
     */
    private static Optional<ModuleLayer> findModuleLayer(ModuleLayer moduleLayer, Configuration cf) {
        return SharedSecrets.getJavaLangAccess().layers(moduleLayer) //
                        .filter(l -> l.configuration() == cf) //
                        .findAny();
    }

    /**
     * See {@code jdk.internal.loader.Loader#findResource(String mn, String name)}.
     */
    @Override
    protected URL findResource(String mn, String name) throws IOException {
        /* For unnamed module, search for resource in class-path */

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Ensure each package is readable from exactly one parent loader: deduplicate shared libraries so they are loaded only in the boot layer or only in the custom layer
  2. Adjust the parent layer configuration passed to VMAccess so overlapping 'requires' edges are not resolved twice
  3. Isolate conflicting modules into separate configurations/VMAccess instances instead of one shared layer

Example fix

# before
# both boot layer and custom layer provide com.example.api
--add-modules com.example.api --module-path custom/

# after
# load com.example.api only via boot layer; custom layer requires it, does not re-export
# (drop the duplicate from the custom module path)
Defensive patterns

Strategy: validation

Validate before calling

Set<String> seen = new HashSet<>();
for (String pn : localPackages) {
    for (ModuleLayer parent : parentLayers) {
        ClassLoader cl = parent.findLoader(parent.findModule(pn).orElseThrow().name());
        // ensure each remote package maps to exactly one loader
    }
}

Try / catch

catch (IllegalStateException e) { if (e.getMessage().contains("multiple loaders")) { list parent layers exposing the package; deduplicate libraries; } }

Prevention

When it happens

Trigger: The module configuration given to HostVMAccessClassLoader resolves modules that 'requires transitive' the same package through two distinct parent loaders/layers (e.g. boot layer plus an custom parent layer that both expose com.example.api). Thrown during loader construction from remotePackage().

Common situations: Layering multiple module layers where a library is visible through two parent loaders (common in app servers, test frameworks, or nested image-build setups); mixing a custom parent ClassLoader that also loads modules from the boot layer.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/7aaa233fa649ecbd. Report an issue: GitHub.