0

I have some strange edge case behavior I want to discuss and solve with you. And as a heads-up: Please do not ask why I want to do something :)

As far as my understanding goes, the JVM loads classes into the heap once they are needed (or forced to load) and once they are loaded, they stay there... That's also the reason why you have to overwrite bytecode though instrumentation to change e.g. a method of a class at runtime once it's loaded.

Now to my problem:

I have written a custom ClassLoader that works fine and is also able to load the class. I verified that by running:

CustomClassLoader classLoader = new CustomClassLoader();
Class<?> clazz = classLoader.loadClass("me.micartey.test.Core");
System.out.println(clazz);
Core core = new Core();

The output is following:

class me.micartey.test.core.Core
java.lang.NoClassDefFoundError: me/micartey/test/core/Core ...

So my suspicion is that classloader may not "share" all classes in the heap... :thinking: (Reflection works but not my goal)

I then tried to set the parent of the current classloader as the parent of my custom classloader and then my custom classloader as the parent of the current classloader. However, this results in a StackOverflowException when trying to load classes that my classloader uses

Preferably, I don't even need to call loadClass and it is able to resolve the class without any reflection etc.

trincot
  • 317,000
  • 35
  • 244
  • 286
micartey
  • 128
  • 6
  • 1
    Could you please provide [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example), so it's possible to run your code – geobreze Apr 05 '23 at 20:24
  • I think you'll have to setup your custom classloader as system default (https://stackoverflow.com/questions/6366288/how-to-change-default-class-loader-in-java `-Djava.system.class.loader=...`) for that to work. Any time you load a class implicitly by doing `new SomeClass()` it would by default ask the system class loader (see `Core.class.getClassLoader()` vs `clazz.getClassLoader()`) which has no idea that there's another class loader that could load the class – zapl Apr 05 '23 at 20:38
  • That might work, but I would like to stay away from java arguments – micartey Apr 06 '23 at 09:16

1 Answers1

3

Classes are not visible across the entire JVM and class loaders are not omnipotent entities knowing every existing class in the JVM. That’s why they have a parent loader to delegate to. Different class loaders may even define (unrelated) classes with the same name.

As the JVM specification puts it:

After creation, a class or interface is determined not by its name alone, but by a pair: its binary name (§4.2.1) and its defining loader.

Also, the assumption that classes always stay forever is not correct. If a (custom) class loader and all of its defined classes become unreachable, they might get garbage collected.

From the Java Language Specification:

An implementation of the Java programming language may unload classes.

A class or interface may be unloaded if and only if its defining class loader may be reclaimed by the garbage collector as discussed in §12.6.

So your current class loader can not see the classes defined by your custom class loader. You can not reverse the parent relationship of these two loaders when the implementation of your custom class loader is defined by your current loader.

If you want to inject the definition of a class into your current class loading context, the simplest approach is

MethodHandles.lookup().defineClass(bytecode);

where bytecode is a byte[] array containing the definiton in the class file format, just like your custom class loader would need.

See also the API of MethodHandles.lookup() and MethodHandles.Lookup.defineClass(…).

To be able to use Core core = new Core(); in the source code after this statement, there must be a definition of the class available at compile-time, but that’s not different to your class loader approach.

You have to be aware of the potential problems of the class not being available before executing the statement that injects it. Consider “When is a Java Class loaded?” and also Does the JVM throw if an unused class is absent? for a practical example

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thank you for your answer! That clarified my knowledge gap. Sadly, I cannot load the class through `MethodHandles.lookup().defineClass(bytecode);` as I need the hash of the class name to be able to resolve the byte array :D (As I said, edge case) And the compile "issue" is avoided by using the `compileOnly` function in gradle. Do you have another idea how I can add my custom classloader to the classloaders that are processed until the class is resolved? – micartey Apr 06 '23 at 09:15
  • 1
    You must know the name of the class beforehand, to be able to call `loadClass` on the class loader anyway, like `loadClass("me.micartey.test.Core")`. So if you know the class name, you also know the hash code. There’s nothing stopping you from doing exactly what your custom class loaded did, just replace the final `[super.]defineClass(…)` call on the class loader with the `MethodHandles.lookup().defineClass(…)` call. – Holger Apr 06 '23 at 09:20
  • When I wanted to try it out, I realized that it only works for java 9 and above. But I am forced to write my class loader with Java 8 support in mind. Is there an alternative method that works on Java 8 as well? – micartey Apr 06 '23 at 09:31
  • 1
    In Java 8, you can still use Reflection to get your current class loader’s `defineClass` method with access override (`setAccessible(true)`) and call it, to define classes in this context. This becomes more restricted with the module system introduced with Java 9. So it’s important to keep in mind that you will have to migrate to a solution like `MethodHandles.lookup().defineClass(…)` in the long term. But in Java 8, hacking into the existing class loader still works. Otherwise, you’d have to use the Instrumentation API, which is more complicated as it separates Java application and Java agent – Holger Apr 06 '23 at 09:37
  • So loading the one class works, however all the other classes that are referenced inside the now loaded class cannot be loaded by the current classloader. I would either need a method to "intercept" all `defineClass` calls of the current classloader or add my custom classloader to some sort of queue that the JVM works of when trying to define a class. Like the last parent of all classloaders which doesn't seem to work... – micartey Apr 06 '23 at 09:48
  • 1
    When you have dependencies between the classes to inject, things get more complicated. The Instrumentation API allows you to [add jar files to the class files](https://docs.oracle.com/en/java/javase/20/docs/api/java.instrument/java/lang/instrument/Instrumentation.html#appendToSystemClassLoaderSearch(java.util.jar.JarFile)) but this doesn’t allow custom logic for constructing class definitions like a custom class loader. – Holger Apr 06 '23 at 10:02
  • 1
    And since this requires a separation between application and class injector anyway, you can do this with class loaders as well. Load the code using the injected classes with the same class loader or a child loader instead of the class loader defining the custom class loader and the class construction facility. – Holger Apr 06 '23 at 10:02