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