JDK-8342642 : Class loading failure due to archived map issue in ModuleLoaderMap.Mapper
  • Type: Bug
  • Component: core-libs
  • Priority: P3
  • Status: New
  • Resolution: Unresolved
  • Submitted: 2024-10-19
  • Updated: 2024-10-25
Related Reports
Relates :  
Description
[~cushon] reported a strange class loader issue with recent JDK mainline change. Below is a test case constructed by [~cushon] for demonstrating the issue:

```
import java.util.List;

class LoadJavacClassTest {
  public static void main(String[] args) throws Exception {
    for (var c :
        List.of(
            "com.sun.tools.javac.processing.JavacProcessingEnvironment")) {
      var cl = ClassLoader.getSystemClassLoader().loadClass(c).getClassLoader();
      System.err.printf("%s loaded from %s\n", c, cl);
      if (cl == null) {
        throw new AssertionError();
      }
    }
  }
}
```
com.sun.tools.javac.processing.JavacProcessingEnvironment is provided by jdk.compiler module and should be loaded by the PlatformClassLoader.

Running with `java -cp <path> LoadJavacClassTest`, the test works as expected.

When running the test with extra `--enable-native-access=ALL-UNNAMED -Djava.lang.Integer.IntegerCache.high=3000`, it fails to load the class JavacProcessingEnvironment.

When running with `--enable-native-access=ALL-UNNAMED -Djava.lang.Integer.IntegerCache.high=3000` and with `-Xshare:off` to disable the default CDS, the test works ok.

I investigated the issue and found it's caused by the archived map in ModuleLoaderMap.Mapper. Will provide details in additional comments.
Comments
A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/21722 Date: 2024-10-25 21:25:54 +0000
25-10-2024

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/21672 Date: 2024-10-23 22:14:13 +0000
23-10-2024

I've given some thoughts on resolving the issue. I think archived String instance doesn't have such issue like the archive Integer instances. I've experimented with using String type for the mapped value in the loader index mapping. https://github.com/openjdk/jdk/compare/master...jianglizhou:jdk:ModuleLoaderMap-Mapper-archived-mapping-issue?expand=1 change resolves the failure. However, this does makes me realize the hidden usage issue with archived Integer cache. We need a more solid solution to address that, especially with AOT cache there will be more pre-initialization and could involve more usages of the cached Integers. I'll file a separate bug for archived Integer cache issue. [~iklam], that probably needs to be evaluated together with your current ongoing AOT cache work when integrating to mainline.
19-10-2024

More info on why it does not fail in following cases: - bin/java -cp /tmp -Xshare:on T - archived Integer cache (contains archived Integer instances) is used - archived classloaders are used - archived module graph is used - archived system module graph classLoaderFunction is used All archived stuff are used. So no issue. - bin/java --enable-native-access=ALL-UNNAMED -cp /tmp -Xshare:on T - archived Integer cache (contains archived Integer instances) is used - archived classloaders are not used - archived packageToModule map is not used - archived module graph is used The archived Integer instances are used, so loader index mapping still works. I think this just works by accident.
19-10-2024

For `java -cp <path> --enable-native-access=ALL-UNNAMED -Djava.lang.Integer.IntegerCache.high=3000 LoadJavacClassTest`: This is the failed case with the CDS archived Java heap objects partially enabled. What's happening at runtime: - archived Integer cache (contains archived Integer instances) is not used - archived classloaders are not used - archived packageToModule map is not used - archived system module graph is used - archived system module graph classLoaderFunction is used ModuleLoaderMap.Mapper implements a classloaderFunction for mapping system modules to builtin classloaders using a hashmap. The hashmap instance is archived in the default CDS archive, as the instance is reachable from the (pre-initialized) ArchivedModuleGraph.classLoaderFunction static field. ModuleLoaderMap$Mapper.apply() (https://github.com/openjdk/jdk/blob/309b929147e7dddfa27879ff31b1eaad271def85/src/java.base/share/classes/jdk/internal/module/ModuleLoaderMap.java#L89) determines the classloader at runtime using the archived hashmap. The hashmap uses boxed Integer type for mapped values, for the loader mapping. ModuleLoaderMap$Mapper.apply() uses `==` to compare the loader index obtained from the hashmap against the <APP|PLATFORM>_CLASSLOADER constants, which are also boxed Integer type. If java.lang.Integer.IntegerCache.high is not the default, archived Integer cache is not used at runtime, cached boxed Integer instances are recreated. When runtime recreates the cached boxed Integers, a "==" comparison would fail when comparing an archived Integer instance (from the archived loader mapping) and a runtime created Integer instance, even when both have the same primitive integer. That can cause apply() to return an incorrect classloader. In this specific test case, the bootloader is incorrectly returned for the jdk.compiler module.
19-10-2024