Resolving super interface in different contexts depending on the child class
Vladimir Ivanov
vladimir.x.ivanov at oracle.com
Sat Feb 24 02:00:53 UTC 2024
Hi Aleksandr,
I'd like to clarify the terminology: JVMS distinguishes initiating and
defining loaders for a class (JVMS-5.3 [1]).
The effects you are looking at are specific to initiating loaders.
Even though a class is loaded only once (when well-behaved loaders are
involved), it becomes visible in all descendant loaders on case-by-case
basis. Each loader should initiate the loading to resolve the class in
its context. (On implementation level it triggers the update of class
loader dictionary.)
Classes are loaded on-demand and precise conditions when it happens in
practice may be far from evident. As an example, I played with the
sketch you provided and here are my observations (when
ScheduledFutureTask implements ScheduledFuture):
* for the following code shape, bytecode verifier triggers class
loading of Delayed class as part of invokevirtual verification (it needs
to prove ScheduledFutureTask <: Delayed):
public static int compare(ScheduledFutureTask o1,
ScheduledFutureTask o2) {
return o1.compareTo(o2);
}
* if compare is adjusted as follows, then checkcast instruction
triggers class loading for non-null object during execution:
public static int compare(ScheduledFutureTask o1, Object o2) {
return o1.compareTo((Delayed)o2);
}
You mentioned that Delayed interface is not loaded by application loader
in the first case, but I wasn't able to reproduce it based on your
description. Please, share more details.
Personally, I was able to avoid class loading (and trigger
has_unloaded_classes_in_signature() == false for compareTo(Delayed))
either by turning off verification (-Xverify:none) or always passing
null as o2 [2]:
public static void main(String[] args) {
ScheduledFutureTask instances0 = new ScheduledFutureTask();
for (int i = 0; i < 20_000; i++) {
compare(instances0, null);
}
}
The issue with null argument is known (inlining failures due to unloaded
class in method signature when corresponding values are always null).
JDK-8294609 [3] improved the situation for most of scenarios, but
covering nulls require more work. It may be improved in the future release.
Best regards,
Vladimir Ivanov
[1] https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-5.html#jvms-5.3
[2] https://gist.github.com/iwanowww/c33e49092bfa0e6a052f28ae139b55eb
[3] https://bugs.openjdk.org/browse/JDK-8294609
On 2/23/24 08:32, Aleksandr Popov wrote:
> Hi! In the following test case I've investigated how Hotspot loads
> classes into the loaders.
> And found out that the inheritance chain could affect the result.
>
> //--------------------------------------------------------------------------
> public class SimpleTest {
> public static int compare(ScheduledFutureTask o1,
> ScheduledFutureTask o2) {
> return o1.compareTo(o2);
> }
>
> public static void main(String[] args) {
> ScheduledFutureTask instances0 = new ScheduledFutureTask();
> ScheduledFutureTask instances1 = new ScheduledFutureTask();
> compare(instances0, instances1);
> }
> }
>
> final class ScheduledFutureTask implements ScheduledFuture<Void> {
> ...
> @Override
> public int compareTo(Delayed o) {
> if (this == o) {
> return 0;
> }
> }
> }
> //--------------------------------------------------------------------------
>
> * If /ScheduledFutureTask/ class implements
> /java.util.concurrent.ScheduledFuture /interface,
>
> which implements /java.util.concurrent.Delayed/ interface: *[application
> class -> core class -> core class],*
> then the /Delayed /interface would be loaded into the boot class loader,
> since it's resolved in the context of the core class.
>
> I've added some logs to track class resolving:
> # parse interfaces of class ScheduledFutureTask, [APP] loader
> # parse interfaces of class java/util/concurrent/ScheduledFuture, [BOOT]
> loader
> # parse interfaces of class java/util/concurrent/Delayed, [BOOT] loader
> # Adding java/util/concurrent/Delayed to dictionary [BOOT]
>
> * If we replace core class in the chain with an application one:
> *[application class -> application class -> core class], *
>
> then the /Delayed /interface would be loaded into both class loaders,
> since it would be resolved in the context of the application class:
>
> # parse interfaces of class ScheduledFutureTask, [application class loader]
> # parse interfaces of class Interface1, [application class loader]
> # parse interfaces of class java/util/concurrent/Delayed, [boot class
> loader]
> # Adding java/util/concurrent/Delayed to dictionary [boot class loader]
> # Adding java/util/concurrent/Delayed to dictionary [application class
> loader]
>
> * Theoretically, this difference could affect the result of the
> has_unloaded_classes_in_signature() call
>
> for the ScheduledFutureTask::compareTo method.
>
> final class ScheduledFutureTask implements ScheduledFuture<Void> {
> ...
> @Override
> public int compareTo(Delayed o) {
> if (this == o) {
> return 0;
> }
> }
> }
>
> Because in the first case (application class -> core class -> core
> class) we have unloaded /java/util/concurrent/Delayed/
> interface in the context of the accessing class, which was loaded into
> the application class loader.
>
> So my question is: why is it correct to have class loaded into different
> class loaders depending on the inheritance chain?
> Or maybe it's intentionally resolved somewhere in the code?
>
> Note that SystemDictionary::check_signature_loaders doesn't add loader
> constraints to the interface, since the caller and callee loaders are
> the same in this case: both application loaders.
>
> Thank you!
More information about the hotspot-dev
mailing list