Classes used in method body are loaded lazily or eagerly depending on method return type

Remi Forax forax at univ-mlv.fr
Tue May 23 22:31:36 UTC 2023


----- Original Message -----
> From: "Сергей Цыпанов" <sergei.tsypanov at yandex.ru>
> To: "core-libs-dev" <core-libs-dev at openjdk.java.net>
> Sent: Tuesday, May 23, 2023 1:20:44 PM
> Subject: Classes used in method body are loaded lazily or eagerly depending on method return type

> Hello,

Hello,

> 
> originally this question was asked here:
> https://stackoverflow.com/q/76260269/12473843,
> the code sample for reproduction will be put below or can be found via the link
> 
> The issue is about eager/lazy loading of a class depending on method return
> type.

It also depends on the content of the methods.
If you modify the code to return null, the NoClassDefFoundError disappears

 public static class ObjectReturner {
   public Object getObject() {
     return null;
   }
 }
 
 public static class BaseClassReturner {
    public BaseClass getObject() {
      return null;
    }
 }

which means that the NoClassDefFoundError comes from the bytecode verifier.

In your original ObjectReturner, the bytecode verifier needs to check if ChildClass is an Object and there is a shortcircuit for that, so ChildClass does not need to be loaded.
But in BaseClassReturner, the bytecode verifier needs to check if ChildClass is a BaseClass, so it has to load ChildClass to know if the superclass is BaseClass.

Another way to see that it's the verifier that triggers the class loading is to use the option "-noverify"
  java -noverify TestLoading

should works !

regards,
Rémi


> If one runs the code below with Java 11-19 it will fail with
> NoClassDefFoundError (this is expected as delete class file for ChildClass):
> 
> java.lang.NoClassDefFoundError: org/example/TestLoading$ChildClass
>	at java.base/java.lang.Class.forName0(Native Method)
>	at java.base/java.lang.Class.forName(Class.java:390)
>	at java.base/java.lang.Class.forName(Class.java:381)
>	at org.example.TestLoading.loadMyClass(TestLoading.java:29)
>	at org.example.TestLoading.main(TestLoading.java:23)
> Caused by: java.lang.ClassNotFoundException: org.example.TestLoading$ChildClass
>	at
>	java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
>	at
>	java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
>	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
>	... 5 more
> 
> As of Java 20 chapter 12.4.1 of JLS states:
> ---------------------------------------------------------
> A class or interface T will be initialized immediately before the first
> occurrence of any one of the following:
> 
> - T is a class and an instance of T is created.
> - a static method declared by T is invoked.
> - a static field declared by T is assigned.
> - a static field declared by T is used and the field is not a constant variable
> (§4.12.4).
> 
> When a class is initialized, its superclasses are initialized (if they have not
> been previously initialized), as well as any superinterfaces (§8.1.5) that
> declare any default methods (§9.4.3) (if they have not been previously
> initialized).
> Initialization of an interface does not, of itself, cause initialization of any
> of its superinterfaces.
> A reference to a static field (§8.3.1.1) causes initialization of only the class
> or interface that actually declares it, even though it might be referred to
> through the name of a subclass, a subinterface, or a class that implements an
> interface.
> Invocation of certain reflective methods in class Class and in package
> java.lang.reflect also causes class or interface initialization.
> A class or interface will not be initialized under any other circumstance.
> ---------------------------------------------------------
> With the code snippet we see that calling
> Class.forName(ObjectReturner.class.getName()) succeeds and
> Class.forName(BaseClassReturner.class.getName()) fails even though both declare
> returning an instance of ChildClass.
> This failure is unexpected as in the code below we don't fulfill any requirement
> for class loading as of JLS 12.4.1, but the JVM still tries to load the class.
> 
> I suspect it might be related to class file validation and/or security, because
> when we run the code with -Xlog:class+init there's a reference to LinkageError
> in loading log:
> 
> loading: org.example.TestLoading$BaseClassReturner...
> [0.277s][info][class,init] Start class verification for:
> org.example.TestLoading$BaseClassReturner
> [0.277s][info][class,init] 771 Initializing
> 'java/lang/ReflectiveOperationException'(no method) (0x0000000800004028)
> [0.277s][info][class,init] 772 Initializing
> 'java/lang/ClassNotFoundException'(no method) (0x0000000800004288)
> [0.277s][info][class,init] 773 Initializing 'java/lang/LinkageError'(no method)
> (0x00000008000044f8)                                  <----
> [0.277s][info][class,init] 774 Initializing 'java/lang/NoClassDefFoundError'(no
> method) (0x0000000800004758)
> [0.277s][info][class,init] Verification for
> org.example.TestLoading$BaseClassReturner has exception pending
> 'java.lang.NoClassDefFoundError org/example/TestLoading$ChildClass'
> [0.277s][info][class,init] End class verification for:
> org.example.TestLoading$BaseClassReturner
> 
> 
> So I've got three questions about this:
> - Does class loading depend on method's return type?
> - Which part of JLS/JVM spec describes eager class loading in this case?
> - Could one point out the particular piece of the VM code responsible for class
> loading in this case?
> 
> Regards,
> Sergey Tsypanov
> 
> Code snippet for reproduction:
> 
> public class TestLoading {
> 
>  public static void main(String[] args) throws Exception {
>    Class.forName(BaseClass.class.getName());
>    URL classFileB = TestLoading.class.getResource(TestLoading.class.getSimpleName()
>    + "$ChildClass.class");
>    if (classFileB != null) {
>      if (!"file".equals(classFileB.getProtocol())) {
>        throw new UnsupportedOperationException();
>      }
>      Path path = new File(classFileB.getPath()).toPath();
>      System.out.println("deleting: " + path);
>      Files.delete(path);
>    }
> 
>    loadMyClass(ObjectReturner.class.getName());
>    loadMyClass(BaseClassReturner.class.getName());
>  }
> 
>  private static void loadMyClass(String name) {
>    System.out.println("loading: " + name + "...");
>    try {
>      Class.forName(name);
>    } catch (Throwable e) {
>      e.printStackTrace(System.out);
>    }
>  }
> 
>  public static class BaseClass {
>  }
> 
>  public static class ChildClass extends BaseClass {
>  }
> 
>  public static class ObjectReturner {
>    public Object getObject() {
>      return new ChildClass();
>    }
>  }
> 
>  public static class BaseClassReturner {
>    public BaseClass getObject() {
>      return new ChildClass();
>    }
>  }
> }


More information about the core-libs-dev mailing list